summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorChris Bessent <lost.dragonist@gmail.com>2020-01-06 05:17:01 -0700
committerGitHub <noreply@github.com>2020-01-06 05:17:01 -0700
commitdfa600996c49c1b23dca884c963dc917bd9cfc0a (patch)
treec6def4277cc962822d0dcde71fa9148e050f693f /src
parente69078e2399a88bda7bb9ffae7a3d57db9a4e9cf (diff)
parentd1a788dfad341b32235abc25c2ba1645f8be1ace (diff)
Merge pull request #954 from ModOrganizer2/Develop
Stage for release 2.2.2
Diffstat (limited to 'src')
-rw-r--r--src/CMakeLists.txt120
-rw-r--r--src/aboutdialog.cpp3
-rw-r--r--src/aboutdialog.ui38
-rw-r--r--src/apiuseraccount.cpp35
-rw-r--r--src/apiuseraccount.h27
-rw-r--r--src/bbcode.cpp9
-rw-r--r--src/browserdialog.cpp23
-rw-r--r--src/categories.cpp52
-rw-r--r--src/categories.h30
-rw-r--r--src/categoriesdialog.cpp7
-rw-r--r--src/categoriesdialog.h6
-rw-r--r--src/colortable.cpp334
-rw-r--r--src/colortable.h39
-rw-r--r--src/directoryrefresher.cpp4
-rw-r--r--src/downloadlist.cpp35
-rw-r--r--src/downloadlist.h3
-rw-r--r--src/downloadlistsortproxy.cpp4
-rw-r--r--src/downloadlistwidget.cpp73
-rw-r--r--src/downloadmanager.cpp220
-rw-r--r--src/downloadmanager.h28
-rw-r--r--src/editexecutablesdialog.cpp256
-rw-r--r--src/editexecutablesdialog.h26
-rw-r--r--src/editexecutablesdialog.ui39
-rw-r--r--src/env.cpp1112
-rw-r--r--src/env.h287
-rw-r--r--src/envmetrics.cpp266
-rw-r--r--src/envmetrics.h81
-rw-r--r--src/envmodule.cpp784
-rw-r--r--src/envmodule.h164
-rw-r--r--src/envsecurity.cpp758
-rw-r--r--src/envsecurity.h75
-rw-r--r--src/envshortcut.cpp374
-rw-r--r--src/envshortcut.h110
-rw-r--r--src/envwindows.cpp238
-rw-r--r--src/envwindows.h111
-rw-r--r--src/executableslist.cpp123
-rw-r--r--src/executableslist.h15
-rw-r--r--src/expanderwidget.cpp54
-rw-r--r--src/expanderwidget.h46
-rw-r--r--src/filedialogmemory.cpp54
-rw-r--r--src/filedialogmemory.h14
-rw-r--r--src/filerenamer.cpp90
-rw-r--r--src/filerenamer.h8
-rw-r--r--src/filterlist.cpp419
-rw-r--r--src/filterlist.h55
-rw-r--r--src/filterwidget.cpp5
-rw-r--r--src/forcedloaddialogwidget.cpp9
-rw-r--r--src/helper.cpp125
-rw-r--r--src/helper.h64
-rw-r--r--src/icondelegate.cpp19
-rw-r--r--src/icondelegate.h18
-rw-r--r--src/ilockedwaitingforprocess.h13
-rw-r--r--src/installationmanager.cpp110
-rw-r--r--src/installationmanager.h4
-rw-r--r--src/instancemanager.cpp59
-rw-r--r--src/instancemanager.h4
-rw-r--r--src/iuserinterface.h9
-rw-r--r--src/json.h190
-rw-r--r--src/listdialog.cpp9
-rw-r--r--src/listdialog.h4
-rw-r--r--src/loadmechanism.cpp276
-rw-r--r--src/loadmechanism.h67
-rw-r--r--src/lockeddialog.cpp71
-rw-r--r--src/lockeddialog.h57
-rw-r--r--src/lockeddialog.ui98
-rw-r--r--src/lockeddialogbase.cpp73
-rw-r--r--src/lockeddialogbase.h62
-rw-r--r--src/logbuffer.cpp278
-rw-r--r--src/logbuffer.h95
-rw-r--r--src/loglist.cpp255
-rw-r--r--src/loglist.h81
-rw-r--r--src/loot.cpp952
-rw-r--r--src/loot.h144
-rw-r--r--src/lootdialog.cpp283
-rw-r--r--src/lootdialog.h79
-rw-r--r--src/lootdialog.ui241
-rw-r--r--src/main.cpp497
-rw-r--r--src/mainwindow.cpp2171
-rw-r--r--src/mainwindow.h107
-rw-r--r--src/mainwindow.ui2375
-rw-r--r--src/messagedialog.cpp6
-rw-r--r--src/moapplication.cpp21
-rw-r--r--src/modconflicticondelegate.cpp160
-rw-r--r--src/modconflicticondelegate.h36
-rw-r--r--src/modflagicondelegate.cpp110
-rw-r--r--src/modflagicondelegate.h15
-rw-r--r--src/modinfo.cpp43
-rw-r--r--src/modinfo.h37
-rw-r--r--src/modinfobackup.cpp1
-rw-r--r--src/modinfobackup.h2
-rw-r--r--src/modinfodialog.cpp116
-rw-r--r--src/modinfodialog.h26
-rw-r--r--src/modinfodialog.ui16
-rw-r--r--src/modinfodialogconflicts.cpp299
-rw-r--r--src/modinfodialogconflicts.h12
-rw-r--r--src/modinfodialogesps.cpp11
-rw-r--r--src/modinfodialogfiletree.cpp236
-rw-r--r--src/modinfodialogfiletree.h24
-rw-r--r--src/modinfodialogfwd.h1
-rw-r--r--src/modinfodialogimages.cpp68
-rw-r--r--src/modinfodialogimages.h3
-rw-r--r--src/modinfodialognexus.cpp13
-rw-r--r--src/modinfodialognexus.h2
-rw-r--r--src/modinfodialogtab.h34
-rw-r--r--src/modinfodialogtextfiles.cpp4
-rw-r--r--src/modinfoforeign.cpp10
-rw-r--r--src/modinfoforeign.h5
-rw-r--r--src/modinfooverwrite.cpp11
-rw-r--r--src/modinfooverwrite.h1
-rw-r--r--src/modinforegular.cpp29
-rw-r--r--src/modinfoseparator.h2
-rw-r--r--src/modinfowithconflictinfo.cpp4
-rw-r--r--src/modinfowithconflictinfo.h3
-rw-r--r--src/modlist.cpp141
-rw-r--r--src/modlist.h7
-rw-r--r--src/modlistsortproxy.cpp334
-rw-r--r--src/modlistsortproxy.h67
-rw-r--r--src/motddialog.cpp2
-rw-r--r--src/nexusinterface.cpp170
-rw-r--r--src/nexusinterface.h1
-rw-r--r--src/nxmaccessmanager.cpp964
-rw-r--r--src/nxmaccessmanager.h221
-rw-r--r--src/organizer_en.ts4338
-rw-r--r--src/organizercore.cpp1379
-rw-r--r--src/organizercore.h86
-rw-r--r--src/organizerproxy.cpp58
-rw-r--r--src/overwriteinfodialog.cpp34
-rw-r--r--src/overwriteinfodialog.h11
-rw-r--r--src/pch.h48
-rw-r--r--src/persistentcookiejar.cpp10
-rw-r--r--src/plugincontainer.cpp37
-rw-r--r--src/pluginlist.cpp729
-rw-r--r--src/pluginlist.h76
-rw-r--r--src/previewdialog.cpp7
-rw-r--r--src/previewdialog.h4
-rw-r--r--src/previewgenerator.cpp5
-rw-r--r--src/problemsdialog.cpp10
-rw-r--r--src/problemsdialog.h4
-rw-r--r--src/problemsdialog.ui2
-rw-r--r--src/processrunner.cpp959
-rw-r--r--src/processrunner.h180
-rw-r--r--src/profile.cpp124
-rw-r--r--src/profile.h14
-rw-r--r--src/profilesdialog.cpp14
-rw-r--r--src/profilesdialog.h4
-rw-r--r--src/qtgroupingproxy.cpp87
-rw-r--r--src/resources/markdown.html292
-rw-r--r--src/sanitychecks.cpp357
-rw-r--r--src/selfupdater.cpp39
-rw-r--r--src/selfupdater.h14
-rw-r--r--src/serverinfo.cpp177
-rw-r--r--src/serverinfo.h62
-rw-r--r--src/settings.cpp2424
-rw-r--r--src/settings.h1094
-rw-r--r--src/settingsdialog.cpp649
-rw-r--r--src/settingsdialog.h152
-rw-r--r--src/settingsdialog.ui1046
-rw-r--r--src/settingsdialogdiagnostics.cpp109
-rw-r--r--src/settingsdialogdiagnostics.h20
-rw-r--r--src/settingsdialoggeneral.cpp215
-rw-r--r--src/settingsdialoggeneral.h29
-rw-r--r--src/settingsdialognexus.cpp397
-rw-r--r--src/settingsdialognexus.h42
-rw-r--r--src/settingsdialogpaths.cpp215
-rw-r--r--src/settingsdialogpaths.h32
-rw-r--r--src/settingsdialogplugins.cpp119
-rw-r--r--src/settingsdialogplugins.h21
-rw-r--r--src/settingsdialogsteam.cpp17
-rw-r--r--src/settingsdialogsteam.h14
-rw-r--r--src/settingsdialogworkarounds.cpp136
-rw-r--r--src/settingsdialogworkarounds.h33
-rw-r--r--src/settingsutilities.cpp331
-rw-r--r--src/settingsutilities.h276
-rw-r--r--src/shared/appconfig.inc1
-rw-r--r--src/shared/directoryentry.cpp30
-rw-r--r--src/shared/error_report.cpp45
-rw-r--r--src/shared/error_report.h19
-rw-r--r--src/shared/util.cpp1835
-rw-r--r--src/shared/util.h404
-rw-r--r--src/singleinstance.cpp21
-rw-r--r--src/spawn.cpp1093
-rw-r--r--src/spawn.h101
-rw-r--r--src/statusbar.cpp73
-rw-r--r--src/statusbar.h14
-rw-r--r--src/stylesheets/Night Eyes.qss9
-rw-r--r--src/stylesheets/Paper Automata.qss1002
-rw-r--r--src/stylesheets/Paper Dark by 6788.qss1026
-rw-r--r--src/stylesheets/Paper Light by 6788.qss1009
-rw-r--r--src/stylesheets/Paper/Automata/Arrows/down.svg97
-rw-r--r--src/stylesheets/Paper/Automata/Arrows/left.svg97
-rw-r--r--src/stylesheets/Paper/Automata/Arrows/right.svg97
-rw-r--r--src/stylesheets/Paper/Automata/Arrows/up.svg97
-rw-r--r--src/stylesheets/Paper/Automata/Toolbar/archives.svg99
-rw-r--r--src/stylesheets/Paper/Automata/Toolbar/executables.svg99
-rw-r--r--src/stylesheets/Paper/Automata/Toolbar/help.svg128
-rw-r--r--src/stylesheets/Paper/Automata/Toolbar/instances.svg98
-rw-r--r--src/stylesheets/Paper/Automata/Toolbar/nexus.svg122
-rw-r--r--src/stylesheets/Paper/Automata/Toolbar/problems.svg132
-rw-r--r--src/stylesheets/Paper/Automata/Toolbar/profiles.svg112
-rw-r--r--src/stylesheets/Paper/Automata/Toolbar/settings.svg95
-rw-r--r--src/stylesheets/Paper/Automata/Toolbar/tools.svg117
-rw-r--r--src/stylesheets/Paper/Automata/Toolbar/update.svg141
-rw-r--r--src/stylesheets/Paper/Automata/background.svg99
-rw-r--r--src/stylesheets/Paper/Automata/backup.svg126
-rw-r--r--src/stylesheets/Paper/Automata/branch.svg99
-rw-r--r--src/stylesheets/Paper/Automata/collapsed.svg97
-rw-r--r--src/stylesheets/Paper/Automata/cross.svg94
-rw-r--r--src/stylesheets/Paper/Automata/dots.svg111
-rw-r--r--src/stylesheets/Paper/Automata/expanded.svg104
-rw-r--r--src/stylesheets/Paper/Automata/folder.svg94
-rw-r--r--src/stylesheets/Paper/Automata/heart.svg111
-rw-r--r--src/stylesheets/Paper/Automata/refresh.svg103
-rw-r--r--src/stylesheets/Paper/Automata/restore-alt.svg126
-rw-r--r--src/stylesheets/Paper/Automata/restore.svg94
-rw-r--r--src/stylesheets/Paper/Automata/run.svg109
-rw-r--r--src/stylesheets/Paper/Automata/shortcut.svg94
-rw-r--r--src/stylesheets/Paper/Automata/sort.svg125
-rw-r--r--src/stylesheets/Paper/Dark/Arrows/down.svg97
-rw-r--r--src/stylesheets/Paper/Dark/Arrows/left.svg97
-rw-r--r--src/stylesheets/Paper/Dark/Arrows/right.svg97
-rw-r--r--src/stylesheets/Paper/Dark/Arrows/up.svg97
-rw-r--r--src/stylesheets/Paper/Dark/Toolbar/archives.svg99
-rw-r--r--src/stylesheets/Paper/Dark/Toolbar/executables.svg99
-rw-r--r--src/stylesheets/Paper/Dark/Toolbar/help.svg128
-rw-r--r--src/stylesheets/Paper/Dark/Toolbar/instances.svg98
-rw-r--r--src/stylesheets/Paper/Dark/Toolbar/nexus.svg122
-rw-r--r--src/stylesheets/Paper/Dark/Toolbar/problems.svg132
-rw-r--r--src/stylesheets/Paper/Dark/Toolbar/profiles.svg112
-rw-r--r--src/stylesheets/Paper/Dark/Toolbar/settings.svg95
-rw-r--r--src/stylesheets/Paper/Dark/Toolbar/tools.svg117
-rw-r--r--src/stylesheets/Paper/Dark/Toolbar/update.svg141
-rw-r--r--src/stylesheets/Paper/Dark/backup.svg128
-rw-r--r--src/stylesheets/Paper/Dark/check-alt.svg94
-rw-r--r--src/stylesheets/Paper/Dark/check.svg94
-rw-r--r--src/stylesheets/Paper/Dark/cross.svg94
-rw-r--r--src/stylesheets/Paper/Dark/dots.svg111
-rw-r--r--src/stylesheets/Paper/Dark/folder.svg94
-rw-r--r--src/stylesheets/Paper/Dark/heart.svg111
-rw-r--r--src/stylesheets/Paper/Dark/highlight.svg205
-rw-r--r--src/stylesheets/Paper/Dark/refresh.svg103
-rw-r--r--src/stylesheets/Paper/Dark/restore-alt.svg125
-rw-r--r--src/stylesheets/Paper/Dark/restore.svg94
-rw-r--r--src/stylesheets/Paper/Dark/run.svg109
-rw-r--r--src/stylesheets/Paper/Dark/shortcut.svg96
-rw-r--r--src/stylesheets/Paper/Dark/sort.svg122
-rw-r--r--src/stylesheets/Paper/Dark/unchecked-alt.svg100
-rw-r--r--src/stylesheets/Paper/Dark/unchecked-disabled.svg94
-rw-r--r--src/stylesheets/Paper/Dark/unchecked.svg100
-rw-r--r--src/stylesheets/Paper/Light/Arrows/down.svg97
-rw-r--r--src/stylesheets/Paper/Light/Arrows/left.svg97
-rw-r--r--src/stylesheets/Paper/Light/Arrows/right.svg97
-rw-r--r--src/stylesheets/Paper/Light/Arrows/up.svg97
-rw-r--r--src/stylesheets/Paper/Light/Toolbar/archives.svg99
-rw-r--r--src/stylesheets/Paper/Light/Toolbar/executables.svg99
-rw-r--r--src/stylesheets/Paper/Light/Toolbar/help.svg128
-rw-r--r--src/stylesheets/Paper/Light/Toolbar/instances.svg98
-rw-r--r--src/stylesheets/Paper/Light/Toolbar/nexus.svg122
-rw-r--r--src/stylesheets/Paper/Light/Toolbar/problems.svg132
-rw-r--r--src/stylesheets/Paper/Light/Toolbar/profiles.svg112
-rw-r--r--src/stylesheets/Paper/Light/Toolbar/settings.svg95
-rw-r--r--src/stylesheets/Paper/Light/Toolbar/tools.svg117
-rw-r--r--src/stylesheets/Paper/Light/Toolbar/update.svg141
-rw-r--r--src/stylesheets/Paper/Light/backup.svg126
-rw-r--r--src/stylesheets/Paper/Light/check-alt.svg94
-rw-r--r--src/stylesheets/Paper/Light/check-white.svg94
-rw-r--r--src/stylesheets/Paper/Light/check.svg94
-rw-r--r--src/stylesheets/Paper/Light/cross.svg94
-rw-r--r--src/stylesheets/Paper/Light/dots.svg111
-rw-r--r--src/stylesheets/Paper/Light/folder.svg94
-rw-r--r--src/stylesheets/Paper/Light/heart.svg111
-rw-r--r--src/stylesheets/Paper/Light/refresh.svg103
-rw-r--r--src/stylesheets/Paper/Light/restore-alt.svg126
-rw-r--r--src/stylesheets/Paper/Light/restore.svg94
-rw-r--r--src/stylesheets/Paper/Light/run.svg109
-rw-r--r--src/stylesheets/Paper/Light/shortcut.svg94
-rw-r--r--src/stylesheets/Paper/Light/sort.svg125
-rw-r--r--src/stylesheets/Paper/Light/unchecked-alt.svg94
-rw-r--r--src/stylesheets/Paper/Light/unchecked-disabled.svg94
-rw-r--r--src/stylesheets/Paper/Light/unchecked-hover.svg94
-rw-r--r--src/stylesheets/Paper/Light/unchecked.svg94
-rw-r--r--src/stylesheets/Transparent-Style-101-Green.qss5
-rw-r--r--src/stylesheets/Transparent-Style-BOS.qss4
-rw-r--r--src/stylesheets/Transparent-Style-Skyrim.qss4
-rw-r--r--src/stylesheets/dark.qss8
-rw-r--r--src/stylesheets/dracula.qss6
-rw-r--r--src/stylesheets/skyrim.qss3
-rw-r--r--src/stylesheets/vs15 Dark-Green.qss10
-rw-r--r--src/stylesheets/vs15 Dark-Orange.qss10
-rw-r--r--src/stylesheets/vs15 Dark-Purple.qss10
-rw-r--r--src/stylesheets/vs15 Dark-Red.qss10
-rw-r--r--src/stylesheets/vs15 Dark-Yellow.qss10
-rw-r--r--src/stylesheets/vs15 Dark.qss10
-rw-r--r--src/syncoverwritedialog.cpp3
-rw-r--r--src/texteditor.cpp9
-rw-r--r--src/transfersavesdialog.cpp15
-rw-r--r--src/tutorials/tutorial_primer_main.js2
-rw-r--r--src/uilocker.cpp617
-rw-r--r--src/uilocker.h96
-rw-r--r--src/usvfsconnector.cpp135
-rw-r--r--src/usvfsconnector.h13
-rw-r--r--src/validationprogressdialog.ui117
-rw-r--r--src/version.rc4
-rw-r--r--src/waitingonclosedialog.cpp71
-rw-r--r--src/waitingonclosedialog.h56
-rw-r--r--src/waitingonclosedialog.ui119
305 files changed, 26471 insertions, 26809 deletions
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index f197211a..d0215930 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -37,7 +37,15 @@ SET(organizer_SRCS
spawn.cpp
singleinstance.cpp
settingsdialog.cpp
+ settingsdialogdiagnostics.cpp
+ settingsdialoggeneral.cpp
+ settingsdialognexus.cpp
+ settingsdialogpaths.cpp
+ settingsdialogplugins.cpp
+ settingsdialogsteam.cpp
+ settingsdialogworkarounds.cpp
settings.cpp
+ settingsutilities.cpp
selfupdater.cpp
selectiondialog.cpp
queryoverwritedialog.cpp
@@ -73,13 +81,9 @@ SET(organizer_SRCS
mainwindow.cpp
main.cpp
loghighlighter.cpp
- logbuffer.cpp
- lockeddialogbase.cpp
- lockeddialog.cpp
- waitingonclosedialog.cpp
+ loglist.cpp
loadmechanism.cpp
installationmanager.cpp
- helper.cpp
filedialogmemory.cpp
executableslist.cpp
editexecutablesdialog.cpp
@@ -110,6 +114,7 @@ SET(organizer_SRCS
previewdialog.cpp
aboutdialog.cpp
modflagicondelegate.cpp
+ modconflicticondelegate.cpp
genericicondelegate.cpp
organizerproxy.cpp
viewmarkingscrollbar.cpp
@@ -128,7 +133,19 @@ SET(organizer_SRCS
apiuseraccount.cpp
filerenamer.cpp
texteditor.cpp
- expanderwidget.cpp
+ env.cpp
+ envmetrics.cpp
+ envmodule.cpp
+ envsecurity.cpp
+ envshortcut.cpp
+ envwindows.cpp
+ colortable.cpp
+ sanitychecks.cpp
+ processrunner.cpp
+ uilocker.cpp
+ loot.cpp
+ lootdialog.cpp
+ filterlist.cpp
shared/windows_error.cpp
shared/error_report.cpp
@@ -145,7 +162,15 @@ SET(organizer_HDRS
spawn.h
singleinstance.h
settingsdialog.h
+ settingsdialogdiagnostics.h
+ settingsdialoggeneral.h
+ settingsdialognexus.h
+ settingsdialogpaths.h
+ settingsdialogplugins.h
+ settingsdialogsteam.h
+ settingsdialogworkarounds.h
settings.h
+ settingsutilities.h
selfupdater.h
selectiondialog.h
queryoverwritedialog.h
@@ -181,13 +206,9 @@ SET(organizer_HDRS
messagedialog.h
mainwindow.h
loghighlighter.h
- logbuffer.h
- lockeddialogbase.h
- lockeddialog.h
- waitingonclosedialog.h
+ loglist.h
loadmechanism.h
installationmanager.h
- helper.h
filedialogmemory.h
executableslist.h
editexecutablesdialog.h
@@ -218,12 +239,12 @@ SET(organizer_HDRS
previewdialog.h
aboutdialog.h
modflagicondelegate.h
+ modconflicticondelegate.h
genericicondelegate.h
organizerproxy.h
viewmarkingscrollbar.h
plugincontainer.h
organizercore.h
- ilockedwaitingforprocess.h
iuserinterface.h
instancemanager.h
usvfsconnector.h
@@ -238,7 +259,19 @@ SET(organizer_HDRS
apiuseraccount.h
filerenamer.h
texteditor.h
- expanderwidget.h
+ env.h
+ envmetrics.h
+ envmodule.h
+ envsecurity.h
+ envshortcut.h
+ envwindows.h
+ colortable.h
+ processrunner.h
+ uilocker.h
+ loot.h
+ lootdialog.h
+ json.h
+ filterlist.h
shared/windows_error.h
shared/error_report.h
@@ -262,8 +295,6 @@ SET(organizer_UIS
modinfodialog.ui
messagedialog.ui
mainwindow.ui
- lockeddialog.ui
- waitingonclosedialog.ui
editexecutablesdialog.ui
credentialsdialog.ui
categoriesdialog.ui
@@ -293,11 +324,13 @@ SET(organizer_RCS
source_group(src REGULAR_EXPRESSION ".*\\.(h|cpp|ui)")
set(application
+ filterlist
iuserinterface
main
mainwindow
moapplication
moshortcut
+ sanitychecks
selfupdater
singleinstance
statusbar
@@ -320,6 +353,8 @@ set(core
organizercore
organizerproxy
apiuseraccount
+ processrunner
+ uilocker
)
set(dialogs
@@ -350,15 +385,23 @@ set(downloads
downloadmanager
)
+set(env
+ env
+ envmetrics
+ envmodule
+ envsecurity
+ envshortcut
+ envwindows
+)
+
set(executables
executableslist
editexecutablesdialog
)
-set(locking
- ilockedwaitingforprocess
- lockeddialog
- lockeddialogbase
+set(loot
+ loot
+ lootdialog
)
set(modinfo
@@ -409,7 +452,18 @@ set(profiles
set(settings
settings
+ settingsutilities
+)
+
+set(settingsdialog
settingsdialog
+ settingsdialogdiagnostics
+ settingsdialoggeneral
+ settingsdialognexus
+ settingsdialogpaths
+ settingsdialogplugins
+ settingsdialogsteam
+ settingsdialogworkarounds
)
set(utilities
@@ -418,7 +472,6 @@ set(utilities
csvbuilder
shared/error_report
eventfilter
- helper
shared/leaktrace
persistentcookiejar
serverinfo
@@ -427,18 +480,20 @@ set(utilities
shared/util
usvfsconnector
shared/windows_error
+ json
)
set(widgets
- expanderwidget
+ colortable
genericicondelegate
filerenamer
filterwidget
icondelegate
lcdnumber
- logbuffer
+ loglist
loghighlighter
modflagicondelegate
+ modconflicticondelegate
modidlineedit
noeditdelegate
qtgroupingproxy
@@ -447,8 +502,9 @@ set(widgets
)
set(src_filters
- application core browser dialogs downloads executables locking modinfo modinfo\\dialog
- modlist plugins previews profiles settings utilities widgets
+ application core browser dialogs downloads env executables loot modinfo
+ modinfo\\dialog modlist plugins previews profiles settings settingsdialog
+ utilities widgets
)
foreach(filter in list ${src_filters})
@@ -520,15 +576,20 @@ LINK_DIRECTORIES(${Boost_LIBRARY_DIRS})
FIND_PACKAGE(zlib REQUIRED)
# TODO FindZlib doesn't find the static zlib library
+# fmt
+find_package(fmt REQUIRED)
+
INCLUDE_DIRECTORIES(${project_path}/uibase/src
${project_path}/bsatk/src
${project_path}/esptk/src
${project_path}/archive/src
+ ${project_path}/lootcli/include
${dependency_project_path}/usvfs/include
${project_path}/game_gamebryo/src/gamebryo
${project_path}/game_gamebryo/src/creation
${project_path}/game_features/src
${project_path}/githubpp/src
+ ${SPDLOG_ROOT}/include
${LZ4_ROOT}/lib)
INCLUDE_DIRECTORIES(shared ${ZLIB_INCLUDE_DIRS})
@@ -551,10 +612,11 @@ ELSE()
ENDIF()
ADD_EXECUTABLE(ModOrganizer WIN32 ${organizer_HDRS} ${organizer_SRCS} ${organizer_UIS} ${organizer_RCS} ${organizer_QRCS} ${organizer_translations_qm})
+
TARGET_LINK_LIBRARIES(ModOrganizer
Qt5::Widgets Qt5::WinExtras Qt5::WebEngineWidgets Qt5::Quick
Qt5::Qml Qt5::QuickWidgets Qt5::Network Qt5::WebSockets
- ${Boost_LIBRARIES}
+ ${Boost_LIBRARIES} fmt::fmt
zlibstatic
uibase esptk bsatk githubpp
${usvfs_name}
@@ -610,6 +672,10 @@ INSTALL(
${qt5bin}/windeployqt.exe --verbose 0 uibase.dll ${windeploy_parameters}
WORKING_DIRECTORY ${CMAKE_INSTALL_PREFIX}/bin
)
+ EXECUTE_PROCESS(COMMAND
+ ${qt5bin}/windeployqt.exe --verbose 0 plugins/bsa_packer.dll ${windeploy_parameters}
+ WORKING_DIRECTORY ${CMAKE_INSTALL_PREFIX}/bin
+ )
file(REMOVE_RECURSE ${CMAKE_INSTALL_PREFIX}/bin/platforms)
file(REMOVE_RECURSE ${CMAKE_INSTALL_PREFIX}/bin/styles)
file(REMOVE_RECURSE ${CMAKE_INSTALL_PREFIX}/bin/dlls/imageformats)
@@ -620,4 +686,6 @@ INSTALL(
)
# qdds.dll needs installing manually as Qt no longer ships with it by default.
-INSTALL(FILES ${CMAKE_CURRENT_SOURCE_DIR}/../qdds.dll DESTINATION bin/dlls/imageformats) \ No newline at end of file
+INSTALL(FILES ${CMAKE_CURRENT_SOURCE_DIR}/../qdds.dll DESTINATION bin/dlls/imageformats)
+
+INSTALL(FILES ${CMAKE_CURRENT_SOURCE_DIR}/resources/markdown.html DESTINATION bin/resources)
diff --git a/src/aboutdialog.cpp b/src/aboutdialog.cpp
index 41fd24f7..6ae7f56d 100644
--- a/src/aboutdialog.cpp
+++ b/src/aboutdialog.cpp
@@ -20,6 +20,7 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#include "aboutdialog.h"
#include "ui_aboutdialog.h"
+#include "util.h"
#include <utility.h>
#include <QApplication>
@@ -85,6 +86,8 @@ AboutDialog::AboutDialog(const QString &version, QWidget *parent)
ui->revisionLabel->setText(ui->revisionLabel->text() + " unknown");
#endif
+
+ ui->usvfsLabel->setText(ui->usvfsLabel->text() + " " + MOShared::getUsvfsVersionString());
ui->licenseText->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont));
}
diff --git a/src/aboutdialog.ui b/src/aboutdialog.ui
index c72a85f8..ec2b754d 100644
--- a/src/aboutdialog.ui
+++ b/src/aboutdialog.ui
@@ -105,6 +105,13 @@
</widget>
</item>
<item>
+ <widget class="QLabel" name="usvfsLabel">
+ <property name="text">
+ <string>usvfs:</string>
+ </property>
+ </widget>
+ </item>
+ <item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
@@ -151,9 +158,9 @@
</spacer>
</item>
<item>
- <widget class="QLabel" name="sourceText">
+ <widget class="LinkLabel" name="sourceText">
<property name="text">
- <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Source code can be found at &lt;a href=&quot;https://github.com/ModOrganizer2/modorganizer&quot;&gt;&lt;span style=&quot; text-decoration: underline; color:#007af4;&quot;&gt;GitHub&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+ <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Source code can be found at &lt;a href=&quot;https://github.com/ModOrganizer2/modorganizer&quot;&gt;GitHub&lt;/a&gt;.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
</widget>
</item>
@@ -208,6 +215,11 @@
<string notr="true">LostDragonist</string>
</property>
</item>
+ <item>
+ <property name="text">
+ <string notr="true">isanae</string>
+ </property>
+ </item>
</widget>
</item>
</layout>
@@ -236,11 +248,6 @@
</item>
<item>
<property name="text">
- <string notr="true">isanae</string>
- </property>
- </item>
- <item>
- <property name="text">
<string notr="true">Project579</string>
</property>
</item>
@@ -322,6 +329,11 @@
</item>
<item>
<property name="text">
+ <string notr="true">Xahtax (German)</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
<string>yohru (Japanese)</string>
</property>
</item>
@@ -464,6 +476,11 @@
</item>
<item>
<property name="text">
+ <string notr="true">reedts</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
<string notr="true">Schilduin</string>
</property>
</item>
@@ -529,6 +546,13 @@
</item>
</layout>
</widget>
+ <customwidgets>
+ <customwidget>
+ <class>LinkLabel</class>
+ <extends>QLabel</extends>
+ <header location="global">linklabel.h</header>
+ </customwidget>
+ </customwidgets>
<resources>
<include location="resources.qrc"/>
</resources>
diff --git a/src/apiuseraccount.cpp b/src/apiuseraccount.cpp
index b901e41a..976dd488 100644
--- a/src/apiuseraccount.cpp
+++ b/src/apiuseraccount.cpp
@@ -1,10 +1,37 @@
#include "apiuseraccount.h"
+QString localizedUserAccountType(APIUserAccountTypes t)
+{
+ switch (t)
+ {
+ case APIUserAccountTypes::Regular:
+ return QObject::tr("Regular");
+
+ case APIUserAccountTypes::Premium:
+ return QObject::tr("Premium");
+
+ case APIUserAccountTypes::None: // fall-through
+ default:
+ return QObject::tr("None");
+ }
+}
+
+
APIUserAccount::APIUserAccount()
: m_type(APIUserAccountTypes::None)
{
}
+bool APIUserAccount::isValid() const
+{
+ return !m_key.isEmpty();
+}
+
+const QString& APIUserAccount::apiKey() const
+{
+ return m_key;
+}
+
const QString& APIUserAccount::id() const
{
return m_id;
@@ -25,6 +52,12 @@ const APILimits& APIUserAccount::limits() const
return m_limits;
}
+APIUserAccount& APIUserAccount::apiKey(const QString& key)
+{
+ m_key = key;
+ return *this;
+}
+
APIUserAccount& APIUserAccount::id(const QString& id)
{
m_id = id;
@@ -63,5 +96,5 @@ bool APIUserAccount::shouldThrottle() const
bool APIUserAccount::exhausted() const
{
- return (remainingRequests() <= 0);
+ return isValid() && (remainingRequests() <= 0);
}
diff --git a/src/apiuseraccount.h b/src/apiuseraccount.h
index 8a238d71..ac8931cf 100644
--- a/src/apiuseraccount.h
+++ b/src/apiuseraccount.h
@@ -18,6 +18,8 @@ enum class APIUserAccountTypes
Premium
};
+QString localizedUserAccountType(APIUserAccountTypes t);
+
/**
* current limits imposed on the user account
@@ -49,17 +51,28 @@ struct APIStats
/**
-* represents a user account on the mod provier website
+* represents a user account on the mod provider website
*/
class APIUserAccount
{
public:
- // when the number of remanining requests is under this number, further
+ // when the number of remaining requests is under this number, further
// requests will be throttled by avoiding non-critical ones
static const int ThrottleThreshold = 200;
APIUserAccount();
+
+ /**
+ * whether the user is logged in
+ */
+ bool isValid() const;
+
+ /**
+ * api key
+ */
+ const QString& apiKey() const;
+
/**
* user id
*/
@@ -82,6 +95,11 @@ public:
/**
+ * sets the api key
+ */
+ APIUserAccount& apiKey(const QString& key);
+
+ /**
* sets the user id
*/
APIUserAccount& id(const QString& id);
@@ -92,7 +110,7 @@ public:
APIUserAccount& name(const QString& name);
/**
- * sets the acount type
+ * sets the account type
*/
APIUserAccount& type(APIUserAccountTypes type);
@@ -120,10 +138,9 @@ public:
bool exhausted() const;
private:
- QString m_id, m_name;
+ QString m_key, m_id, m_name;
APIUserAccountTypes m_type;
APILimits m_limits;
- APIStats m_stats;
};
#endif // APIUSERACCOUNT_H
diff --git a/src/bbcode.cpp b/src/bbcode.cpp
index 323dd128..d9b7debd 100644
--- a/src/bbcode.cpp
+++ b/src/bbcode.cpp
@@ -18,13 +18,13 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
*/
#include "bbcode.h"
-
+#include <log.h>
#include <QRegExp>
#include <map>
-
namespace BBCode {
+namespace log = MOBase::log;
class BBCodeMap {
@@ -88,7 +88,7 @@ public:
return temp.replace(tagIter->second.first, QString("<font style=\"color: #%1;\">%2</font>").arg(color, content));
}
} else {
- qWarning("don't know how to deal with tag %s", qUtf8Printable(tagName));
+ log::warn("don't know how to deal with tag {}", tagName);
}
} else {
if (tagName == "*") {
@@ -99,8 +99,7 @@ public:
} else {
// expression doesn't match. either the input string is invalid
// or the expression is
- qWarning("%s doesn't match the expression for %s",
- qUtf8Printable(temp), qUtf8Printable(tagName));
+ log::warn("{} doesn't match the expression for {}", temp, tagName);
length = 0;
return QString();
}
diff --git a/src/browserdialog.cpp b/src/browserdialog.cpp
index e186ad63..72cb8862 100644
--- a/src/browserdialog.cpp
+++ b/src/browserdialog.cpp
@@ -24,9 +24,10 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#include "messagedialog.h"
#include "report.h"
#include "persistentcookiejar.h"
+#include "settings.h"
#include <utility.h>
-#include "settings.h"
+#include <log.h>
#include <QWebEngineSettings>
#include <QNetworkCookieJar>
@@ -35,9 +36,9 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#include <QInputDialog>
#include <QWebEngineHistory>
#include <QDir>
-#include <QDesktopWidget>
#include <QKeyEvent>
+using namespace MOBase;
BrowserDialog::BrowserDialog(QWidget *parent)
@@ -48,7 +49,7 @@ BrowserDialog::BrowserDialog(QWidget *parent)
ui->setupUi(this);
m_AccessManager->setCookieJar(new PersistentCookieJar(
- QDir::fromNativeSeparators(Settings::instance().getCacheDirectory() + "/cookies.dat")));
+ QDir::fromNativeSeparators(Settings::instance().paths().cache() + "/cookies.dat")));
Qt::WindowFlags flags = windowFlags() | Qt::WindowMaximizeButtonHint | Qt::WindowMinimizeButtonHint;
Qt::WindowFlags helpFlag = Qt::WindowContextHelpButtonHint;
@@ -71,7 +72,7 @@ BrowserDialog::~BrowserDialog()
void BrowserDialog::closeEvent(QCloseEvent *event)
{
-// m_AccessManager->showCookies();
+ Settings::instance().geometry().saveGeometry(this);
QDialog::closeEvent(event);
}
@@ -125,6 +126,7 @@ void BrowserDialog::urlChanged(const QUrl &url)
void BrowserDialog::openUrl(const QUrl &url)
{
if (isHidden()) {
+ Settings::instance().geometry().restoreGeometry(this);
show();
}
openInNewTab(url);
@@ -138,9 +140,8 @@ void BrowserDialog::maximizeWidth()
int contentWidth = getCurrentView()->page()->contentsSize().width();
- QDesktopWidget screen;
- int currentScreen = screen.screenNumber(this);
- int screenWidth = screen.screenGeometry(currentScreen).size().width();
+ QScreen* screen = this->window()->windowHandle()->screen();
+ int screenWidth = screen->geometry().size().width();
int targetWidth = std::min<int>(std::max<int>(viewportWidth, contentWidth) + frameWidth, screenWidth);
this->resize(targetWidth, height());
@@ -192,12 +193,12 @@ void BrowserDialog::unsupportedContent(QNetworkReply *reply)
try {
QWebEnginePage *page = qobject_cast<QWebEnginePage*>(sender());
if (page == nullptr) {
- qCritical("sender not a page");
+ log::error("sender not a page");
return;
}
BrowserView *view = qobject_cast<BrowserView*>(page->view());
if (view == nullptr) {
- qCritical("no view?");
+ log::error("no view?");
return;
}
@@ -206,14 +207,14 @@ void BrowserDialog::unsupportedContent(QNetworkReply *reply)
if (isVisible()) {
MessageDialog::showMessage(tr("failed to start download"), this);
}
- qCritical("exception downloading unsupported content: %s", e.what());
+ log::error("exception downloading unsupported content: {}", e.what());
}
}
void BrowserDialog::downloadRequested(const QNetworkRequest &request)
{
- qCritical("download request %s ignored", request.url().toString().toUtf8().constData());
+ log::error("download request {} ignored", request.url().toString());
}
diff --git a/src/categories.cpp b/src/categories.cpp
index 9e5fa9f7..bb37483b 100644
--- a/src/categories.cpp
+++ b/src/categories.cpp
@@ -21,6 +21,7 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#include <utility.h>
#include <report.h>
+#include <log.h>
#include <QObject>
#include <QFile>
@@ -61,8 +62,9 @@ void CategoryFactory::loadCategories()
++lineNum;
QList<QByteArray> cells = line.split('|');
if (cells.count() != 4) {
- qCritical("invalid category line %d: %s (%d cells)",
- lineNum, line.constData(), cells.count());
+ log::error(
+ "invalid category line {}: {} ({} cells)",
+ lineNum, line.constData(), cells.count());
} else {
std::vector<int> nexusIDs;
if (cells[2].length() > 0) {
@@ -72,7 +74,7 @@ void CategoryFactory::loadCategories()
bool ok = false;
int temp = iter->toInt(&ok);
if (!ok) {
- qCritical("invalid category id %s", iter->constData());
+ log::error("invalid category id {}", iter->constData());
}
nexusIDs.push_back(temp);
}
@@ -82,8 +84,7 @@ void CategoryFactory::loadCategories()
int id = cells[0].toInt(&cell0Ok);
int parentID = cells[3].trimmed().toInt(&cell3Ok);
if (!cell0Ok || !cell3Ok) {
- qCritical("invalid category line %d: %s",
- lineNum, line.constData());
+ log::error("invalid category line {}: {}", lineNum, line.constData());
}
addCategory(id, QString::fromUtf8(cells[1].constData()), nexusIDs, parentID);
}
@@ -294,7 +295,7 @@ bool CategoryFactory::isDecendantOf(int id, int parentID) const
return isDecendantOf(m_Categories[index].m_ParentID, parentID);
}
} else {
- qWarning("%d is no valid category id", id);
+ log::warn("{} is no valid category id", id);
return false;
}
}
@@ -319,6 +320,41 @@ QString CategoryFactory::getCategoryName(unsigned int index) const
return m_Categories[index].m_Name;
}
+QString CategoryFactory::getSpecialCategoryName(SpecialCategories type) const
+{
+ QString label;
+ switch (type)
+ {
+ case Checked: label = QObject::tr("Active"); break;
+ case UpdateAvailable: label = QObject::tr("Update available"); break;
+ case HasCategory: label = QObject::tr("Has category"); break;
+ case Conflict: label = QObject::tr("Conflicted"); break;
+ case Endorsed: label = QObject::tr("Endorsed"); break;
+ case Backup: label = QObject::tr("Has backup"); break;
+ case Managed: label = QObject::tr("Managed"); break;
+ case HasGameData: label = QObject::tr("Has valid game data"); break;
+ case HasNexusID: label = QObject::tr("Has Nexus ID"); break;
+ case Tracked: label = QObject::tr("Tracked on Nexus"); break;
+ default: return {};
+ }
+ return QString("<%1>").arg(label);
+}
+
+QString CategoryFactory::getCategoryNameByID(int id) const
+{
+ auto itor = m_IDMap.find(id);
+
+ if (itor == m_IDMap.end()) {
+ return getSpecialCategoryName(static_cast<SpecialCategories>(id));
+ } else {
+ const auto index = itor->second;
+ if (index >= m_Categories.size()) {
+ return {};
+ }
+
+ return m_Categories[index].m_Name;
+ }
+}
int CategoryFactory::getCategoryID(unsigned int index) const
{
@@ -359,10 +395,10 @@ unsigned int CategoryFactory::resolveNexusID(int nexusID) const
{
std::map<int, unsigned int>::const_iterator iter = m_NexusMap.find(nexusID);
if (iter != m_NexusMap.end()) {
- qDebug("nexus category id %d maps to internal %d", nexusID, iter->second);
+ log::debug("nexus category id {} maps to internal {}", nexusID, iter->second);
return iter->second;
} else {
- qDebug("nexus category id %d not mapped", nexusID);
+ log::debug("nexus category id {} not mapped", nexusID);
return 0U;
}
}
diff --git a/src/categories.h b/src/categories.h
index 48e0b44b..6b27c6a7 100644
--- a/src/categories.h
+++ b/src/categories.h
@@ -37,23 +37,21 @@ class CategoryFactory {
friend class CategoriesDialog;
public:
-
- static const int CATEGORY_NONE = 0;
-
- static const int CATEGORY_SPECIAL_FIRST = 10000;
- static const int CATEGORY_SPECIAL_CHECKED = CATEGORY_SPECIAL_FIRST;
- static const int CATEGORY_SPECIAL_UNCHECKED = 10001;
- static const int CATEGORY_SPECIAL_UPDATEAVAILABLE = 10002;
- static const int CATEGORY_SPECIAL_NOCATEGORY = 10003;
- static const int CATEGORY_SPECIAL_CONFLICT = 10004;
- static const int CATEGORY_SPECIAL_NOTENDORSED = 10005;
- static const int CATEGORY_SPECIAL_BACKUP = 10006;
- static const int CATEGORY_SPECIAL_MANAGED = 10007;
- static const int CATEGORY_SPECIAL_UNMANAGED = 10008;
-
+ enum SpecialCategories
+ {
+ Checked = 10000,
+ UpdateAvailable,
+ HasCategory,
+ Conflict,
+ Endorsed,
+ Backup,
+ Managed,
+ HasGameData,
+ HasNexusID,
+ Tracked
+ };
public:
-
struct Category {
Category(int sortValue, int id, const QString &name, const std::vector<int> &nexusIDs, int parentID)
: m_SortValue(sortValue), m_ID(id), m_Name(name), m_HasChildren(false),
@@ -142,6 +140,8 @@ public:
* @return QString name of the category
**/
QString getCategoryName(unsigned int index) const;
+ QString getSpecialCategoryName(SpecialCategories type) const;
+ QString getCategoryNameByID(int id) const;
/**
* @brief look up the id of a category by its index
diff --git a/src/categoriesdialog.cpp b/src/categoriesdialog.cpp
index 881179a4..b5194bf0 100644
--- a/src/categoriesdialog.cpp
+++ b/src/categoriesdialog.cpp
@@ -21,6 +21,7 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#include "ui_categoriesdialog.h"
#include "categories.h"
#include "utility.h"
+#include "settings.h"
#include <QItemDelegate>
#include <QRegExpValidator>
#include <QLineEdit>
@@ -109,6 +110,12 @@ CategoriesDialog::~CategoriesDialog()
delete ui;
}
+int CategoriesDialog::exec()
+{
+ GeometrySaver gs(Settings::instance(), this);
+ return QDialog::exec();
+}
+
void CategoriesDialog::cellChanged(int row, int)
{
diff --git a/src/categoriesdialog.h b/src/categoriesdialog.h
index 72d2154d..c743c157 100644
--- a/src/categoriesdialog.h
+++ b/src/categoriesdialog.h
@@ -33,12 +33,16 @@ class CategoriesDialog;
class CategoriesDialog : public MOBase::TutorableDialog
{
Q_OBJECT
-
+
public:
explicit CategoriesDialog(QWidget *parent = 0);
~CategoriesDialog();
+ // also saves and restores geometry
+ //
+ int exec() override;
+
/**
* @brief store changes here to the global categories store (categories.h)
*
diff --git a/src/colortable.cpp b/src/colortable.cpp
new file mode 100644
index 00000000..b1e4ef6c
--- /dev/null
+++ b/src/colortable.cpp
@@ -0,0 +1,334 @@
+#include "colortable.h"
+#include "modflagicondelegate.h"
+#include "modconflicticondelegate.h"
+#include "settings.h"
+
+class ColorItem;
+ColorItem* colorItemForRow(QTableWidget* table, int row);
+
+void paintBackground(
+ QTableWidget* table, QPainter* p, const QStyleOptionViewItem& option,
+ const QModelIndex& index);
+
+
+// delegate for the sample text column; paints the background color
+//
+class ColoredBackgroundDelegate : public QStyledItemDelegate
+{
+public:
+ ColoredBackgroundDelegate(QTableWidget* table)
+ : m_table(table)
+ {
+ }
+
+ void paint(
+ QPainter* p, const QStyleOptionViewItem& option,
+ const QModelIndex& index) const override
+ {
+ paintBackground(m_table, p, option, index);
+
+ QStyleOptionViewItem itemOption(option);
+ initStyleOption(&itemOption, index);
+
+ // paint the default stuff like text, but override the state to avoid
+ // destroying the background for selected items, etc.
+ itemOption.state = QStyle::State_Enabled;
+
+ QStyledItemDelegate::paint(p, itemOption, index);
+ }
+
+private:
+ QTableWidget* m_table;
+};
+
+
+// delegate for the icons column; paints the background and icons
+//
+class FakeModFlagIconDelegate : public ModFlagIconDelegate
+{
+public:
+ explicit FakeModFlagIconDelegate(QTableWidget* table)
+ : m_table(table)
+ {
+ }
+
+ void paint(
+ QPainter* painter, const QStyleOptionViewItem& option,
+ const QModelIndex& index) const override
+ {
+ paintBackground(m_table, painter, option, index);
+ ModFlagIconDelegate::paintIcons(painter, option, index, getIcons(index));
+ }
+
+protected:
+ QList<QString> getIcons(const QModelIndex& index) const override
+ {
+ const auto flags = {
+ ModInfo::FLAG_BACKUP,
+ ModInfo::FLAG_NOTENDORSED,
+ ModInfo::FLAG_NOTES,
+ ModInfo::FLAG_ALTERNATE_GAME
+ };
+
+ return getIconsForFlags(flags, false);
+ }
+
+ size_t getNumIcons(const QModelIndex& index) const override
+ {
+ return getIcons(index).size();
+ }
+
+private:
+ QTableWidget* m_table;
+};
+
+
+// delegate for the icons column; paints the background and icons
+//
+class FakeModConflictIconDelegate : public ModConflictIconDelegate
+{
+public:
+ explicit FakeModConflictIconDelegate(QTableWidget* table)
+ : m_table(table)
+ {
+ }
+
+ void paint(
+ QPainter* painter, const QStyleOptionViewItem& option,
+ const QModelIndex& index) const override
+ {
+ paintBackground(m_table, painter, option, index);
+ ModFlagIconDelegate::paintIcons(painter, option, index, getIcons(index));
+ }
+
+protected:
+ QList<QString> getIcons(const QModelIndex& index) const override
+ {
+ const auto flags = {
+ ModInfo::FLAG_CONFLICT_MIXED,
+ ModInfo::FLAG_ARCHIVE_LOOSE_CONFLICT_OVERWRITE,
+ ModInfo::FLAG_ARCHIVE_LOOSE_CONFLICT_OVERWRITTEN,
+ ModInfo::FLAG_ARCHIVE_CONFLICT_MIXED
+ };
+
+ return getIconsForFlags(flags, false);
+ }
+
+ size_t getNumIcons(const QModelIndex& index) const override
+ {
+ return getIcons(index).size();
+ }
+
+private:
+ QTableWidget* m_table;
+};
+
+
+// item used in the first column of the table
+//
+class ColorItem : public QTableWidgetItem
+{
+public:
+ ColorItem(
+ QString caption, QColor defaultColor,
+ std::function<QColor ()> get,
+ std::function<void (const QColor&)> commit) :
+ m_caption(std::move(caption)), m_default(defaultColor),
+ m_get(get), m_commit(commit)
+ {
+ setText(m_caption);
+ set(get());
+ }
+
+ // color caption
+ //
+ const QString& caption() const
+ {
+ return m_caption;
+ }
+
+ // the current color
+ //
+ QColor get() const
+ {
+ return m_temp;
+ }
+
+ // sets the current color, commit() must be called to save it
+ //
+ bool set(const QColor& c)
+ {
+ if (m_temp != c) {
+ m_temp = c;
+ return true;
+ }
+
+ return false;
+ }
+
+ // resets the current color, commit() must be called to save it
+ //
+ bool reset()
+ {
+ return set(m_default);
+ }
+
+ // saves the current color
+ //
+ void commit()
+ {
+ m_commit(m_temp);
+ }
+
+private:
+ const QString m_caption;
+ const QColor m_default;
+ std::function<QColor ()> m_get;
+ std::function<void (const QColor&)> m_commit;
+ QColor m_temp;
+};
+
+
+ColorItem* colorItemForRow(QTableWidget* table, int row)
+{
+ return dynamic_cast<ColorItem*>(table->item(row, 0));
+}
+
+template <class F>
+void forEachColorItem(QTableWidget* table, F&& f)
+{
+ const auto rowCount = table->rowCount();
+
+ for (int i=0; i<rowCount; ++i) {
+ if (auto* ci=colorItemForRow(table, i)) {
+ f(ci);
+ }
+ }
+}
+
+void paintBackground(
+ QTableWidget* table, QPainter* p, const QStyleOptionViewItem& option,
+ const QModelIndex& index)
+{
+ if (auto* ci=colorItemForRow(table, index.row())) {
+ p->save();
+ p->fillRect(option.rect, ci->get());
+ p->restore();
+ }
+}
+
+
+ColorTable::ColorTable(QWidget* parent)
+ : QTableWidget(parent), m_settings(nullptr)
+{
+ setColumnCount(4);
+ setHorizontalHeaderLabels({"", "", "", ""});
+
+ setItemDelegateForColumn(1, new ColoredBackgroundDelegate(this));
+ setItemDelegateForColumn(2, new FakeModConflictIconDelegate(this));
+ setItemDelegateForColumn(3, new FakeModFlagIconDelegate(this));
+
+ connect(
+ this, &QTableWidget::cellActivated,
+ [&]{ onColorActivated(); });
+}
+
+void ColorTable::load(Settings& s)
+{
+ m_settings = &s;
+
+ addColor(
+ QObject::tr("Is overwritten (loose files)"),
+ QColor(0, 255, 0, 64),
+ [this]{ return m_settings->colors().modlistOverwrittenLoose(); },
+ [this](auto&& v){ m_settings->colors().setModlistOverwrittenLoose(v); });
+
+ addColor(
+ QObject::tr("Is overwriting (loose files)"),
+ QColor(255, 0, 0, 64),
+ [this]{ return m_settings->colors().modlistOverwritingLoose(); },
+ [this](auto&& v){ m_settings->colors().setModlistOverwritingLoose(v); });
+
+ addColor(
+ QObject::tr("Is overwritten (archives)"),
+ QColor(0, 255, 255, 64),
+ [this]{ return m_settings->colors().modlistOverwrittenArchive(); },
+ [this](auto&& v){ m_settings->colors().setModlistOverwrittenArchive(v); });
+
+ addColor(
+ QObject::tr("Is overwriting (archives)"),
+ QColor(255, 0, 255, 64),
+ [this]{ return m_settings->colors().modlistOverwritingArchive(); },
+ [this](auto&& v){ m_settings->colors().setModlistOverwritingArchive(v); });
+
+ addColor(
+ QObject::tr("Mod contains selected plugin"),
+ QColor(0, 0, 255, 64),
+ [this]{ return m_settings->colors().modlistContainsPlugin(); },
+ [this](auto&& v){ m_settings->colors().setModlistContainsPlugin(v); });
+
+ addColor(
+ QObject::tr("Plugin is contained in selected mod"),
+ QColor(0, 0, 255, 64),
+ [this]{ return m_settings->colors().pluginListContained(); },
+ [this](auto&& v){ m_settings->colors().setPluginListContained(v); });
+}
+
+void ColorTable::resetColors()
+{
+ bool changed = false;
+
+ forEachColorItem(this, [&](auto* item) {
+ if (item->reset()) {
+ changed = true;
+ }
+ });
+
+ if (changed) {
+ update();
+ }
+}
+
+void ColorTable::commitColors()
+{
+ forEachColorItem(this, [](auto* item) {
+ item->commit();
+ });
+}
+
+void ColorTable::addColor(
+ const QString& text, const QColor& defaultColor,
+ std::function<QColor ()> get,
+ std::function<void (const QColor&)> commit)
+{
+ const auto r = rowCount();
+ setRowCount(r + 1);
+
+ setItem(r, 0, new ColorItem(text, defaultColor, get, commit));
+ setItem(r, 1, new QTableWidgetItem("Text"));
+ setItem(r, 2, new QTableWidgetItem);
+
+ resizeColumnsToContents();
+}
+
+void ColorTable::onColorActivated()
+{
+ const auto rows = selectionModel()->selectedRows();
+ if (rows.isEmpty()) {
+ return;
+ }
+
+ const auto row = rows[0].row();
+ auto* ci = colorItemForRow(this, row);
+ if (!ci) {
+ return;
+ }
+
+ const QColor result = QColorDialog::getColor(
+ ci->get(), topLevelWidget(), ci->caption(), QColorDialog::ShowAlphaChannel);
+
+ if (result.isValid()) {
+ ci->set(result);
+ update(model()->index(row, 1));
+ }
+}
diff --git a/src/colortable.h b/src/colortable.h
new file mode 100644
index 00000000..039b6024
--- /dev/null
+++ b/src/colortable.h
@@ -0,0 +1,39 @@
+#ifndef COLORTABLE_H
+#define COLORTABLE_H
+
+#include <QTableWidget>
+
+class Settings;
+
+// a QTableWidget to view and modify color settings
+//
+class ColorTable : public QTableWidget
+{
+public:
+ ColorTable(QWidget* parent=nullptr);
+
+ // adds colors to the table from the settings
+ //
+ void load(Settings& s);
+
+ // resets the colors to their default values; commitColors() must be called
+ // to save them
+ //
+ void resetColors();
+
+ // commits any changes
+ //
+ void commitColors();
+
+private:
+ Settings* m_settings;
+
+ void addColor(
+ const QString& text, const QColor& defaultColor,
+ std::function<QColor ()> get,
+ std::function<void (const QColor&)> commit);
+
+ void onColorActivated();
+};
+
+#endif // COLORTABLE_H
diff --git a/src/directoryrefresher.cpp b/src/directoryrefresher.cpp
index 3ce4691b..87305599 100644
--- a/src/directoryrefresher.cpp
+++ b/src/directoryrefresher.cpp
@@ -127,7 +127,7 @@ void DirectoryRefresher::addModFilesToStructure(DirectoryEntry *directoryStructu
FilesOrigin &origin = directoryStructure->createOrigin(ToWString(modName), directoryW, priority);
for (const QString &filename : stealFiles) {
if (filename.isEmpty()) {
- qWarning("Trying to find file with no name");
+ log::warn("Trying to find file with no name");
continue;
}
QFileInfo fileInfo(filename);
@@ -143,7 +143,7 @@ void DirectoryRefresher::addModFilesToStructure(DirectoryEntry *directoryStructu
QString warnStr = fileInfo.absolutePath();
if (warnStr.isEmpty())
warnStr = filename;
- qWarning("file not found: %s", qUtf8Printable(warnStr));
+ log::warn("file not found: {}", warnStr);
}
}
} else {
diff --git a/src/downloadlist.cpp b/src/downloadlist.cpp
index 5e698e0e..99347a79 100644
--- a/src/downloadlist.cpp
+++ b/src/downloadlist.cpp
@@ -19,12 +19,14 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#include "downloadlist.h"
#include "downloadmanager.h"
+#include <utility.h>
+#include <log.h>
#include <QEvent>
#include <QColor>
#include <QIcon>
-
#include <QSortFilterProxyModel>
+using namespace MOBase;
DownloadList::DownloadList(DownloadManager *manager, QObject *parent)
: QAbstractTableModel(parent), m_Manager(manager)
@@ -75,6 +77,7 @@ QVariant DownloadList::headerData(int section, Qt::Orientation orientation, int
case COL_SIZE: return tr("Size");
case COL_STATUS: return tr("Status");
case COL_FILETIME: return tr("Filetime");
+ case COL_SOURCEGAME: return tr("Source Game");
default: return QVariant();
}
} else {
@@ -116,11 +119,17 @@ QVariant DownloadList::data(const QModelIndex &index, int role) const
if (m_Manager->isInfoIncomplete(index.row())) {
return {};
} else {
- const MOBase::ModRepositoryFileInfo *info = m_Manager->getFileInfo(index.row());
return QString("%1").arg(m_Manager->getModID(index.row()));
}
}
- case COL_SIZE: return sizeFormat(m_Manager->getFileSize(index.row()));
+ case COL_SOURCEGAME: {
+ if (m_Manager->isInfoIncomplete(index.row())) {
+ return {};
+ } else {
+ return QString("%1").arg(m_Manager->getDisplayGameName(index.row()));
+ }
+ }
+ case COL_SIZE: return MOBase::localizedByteSize(m_Manager->getFileSize(index.row()));
case COL_FILETIME: return m_Manager->getFileTime(index.row());
case COL_STATUS:
switch (m_Manager->getState(index.row())) {
@@ -192,23 +201,5 @@ void DownloadList::update(int row)
else if (row < this->rowCount())
emit dataChanged(this->index(row, 0, QModelIndex()), this->index(row, this->columnCount(QModelIndex())-1, QModelIndex()));
else
- qCritical("invalid row %d in download list, update failed", row);
-}
-
-QString DownloadList::sizeFormat(quint64 size) const
-{
- qreal calc = size;
- QStringList list;
- list << "MB" << "GB" << "TB";
-
- QStringListIterator i(list);
- QString unit("KB");
-
- calc /= 1024.0;
- while (calc >= 1024.0 && i.hasNext()) {
- unit = i.next();
- calc /= 1024.0;
- }
-
- return QString().setNum(calc, 'f', 2) + " " + unit;
+ log::error("invalid row {} in download list, update failed", row);
}
diff --git a/src/downloadlist.h b/src/downloadlist.h
index 6f63f0c8..eb2bbc55 100644
--- a/src/downloadlist.h
+++ b/src/downloadlist.h
@@ -43,6 +43,7 @@ public:
COL_MODNAME,
COL_VERSION,
COL_ID,
+ COL_SOURCEGAME,
// number of columns
COL_COUNT
@@ -99,8 +100,6 @@ private:
DownloadManager *m_Manager;
bool m_MetaDisplay;
-
- QString sizeFormat(quint64 size) const;
};
#endif // DOWNLOADLIST_H
diff --git a/src/downloadlistsortproxy.cpp b/src/downloadlistsortproxy.cpp
index 7bda139b..6209a721 100644
--- a/src/downloadlistsortproxy.cpp
+++ b/src/downloadlistsortproxy.cpp
@@ -96,6 +96,8 @@ bool DownloadListSortProxy::lessThan(const QModelIndex &left,
return m_Manager->getFileSize(left.row()) < m_Manager->getFileSize(right.row());
} else if (left.column() == DownloadList::COL_FILETIME) {
return m_Manager->getFileTime(left.row()) < m_Manager->getFileTime(right.row());
+ } else if (left.column() == DownloadList::COL_SOURCEGAME) {
+ return m_Manager->getDisplayGameName(left.row()) < m_Manager->getDisplayGameName(right.row());
} else {
return leftIndex < rightIndex;
}
@@ -110,7 +112,7 @@ bool DownloadListSortProxy::filterAcceptsRow(int sourceRow, const QModelIndex&)
if (m_CurrentFilter.length() == 0) {
return true;
} else if (sourceRow < m_Manager->numTotalDownloads()) {
- QString displayedName = Settings::instance().metaDownloads()
+ QString displayedName = Settings::instance().interface().metaDownloads()
? m_Manager->getDisplayName(sourceRow)
: m_Manager->getFileName(sourceRow);
return displayedName.contains(m_CurrentFilter, Qt::CaseInsensitive);
diff --git a/src/downloadlistwidget.cpp b/src/downloadlistwidget.cpp
index c75ecdd2..ac37b0ee 100644
--- a/src/downloadlistwidget.cpp
+++ b/src/downloadlistwidget.cpp
@@ -19,6 +19,7 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#include "downloadlist.h"
#include "downloadlistwidget.h"
+#include <log.h>
#include <QPainter>
#include <QMouseEvent>
#include <QMenu>
@@ -29,6 +30,8 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#include <QCheckBox>
#include <QWidgetAction>
+using namespace MOBase;
+
void DownloadProgressDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
{
QModelIndex sourceIndex = m_SortProxy->mapToSource(index);
@@ -132,6 +135,7 @@ void DownloadListWidget::setManager(DownloadManager *manager)
header()->hideSection(DownloadList::COL_MODNAME);
header()->hideSection(DownloadList::COL_VERSION);
header()->hideSection(DownloadList::COL_ID);
+ header()->hideSection(DownloadList::COL_SOURCEGAME);
}
void DownloadListWidget::setSourceModel(DownloadList *sourceModel)
@@ -201,40 +205,49 @@ void DownloadListWidget::onCustomContextMenu(const QPoint &point)
QModelIndex index = indexAt(point);
bool hidden = false;
- if (index.row() >= 0) {
- m_ContextRow = qobject_cast<QSortFilterProxyModel*>(model())->mapToSource(index).row();
- DownloadManager::DownloadState state = m_Manager->getState(m_ContextRow);
- hidden = m_Manager->isHidden(m_ContextRow);
+ try
+ {
+ if (index.row() >= 0) {
+ m_ContextRow = qobject_cast<QSortFilterProxyModel*>(model())->mapToSource(index).row();
+ DownloadManager::DownloadState state = m_Manager->getState(m_ContextRow);
- if (state >= DownloadManager::STATE_READY) {
- menu.addAction(tr("Install"), this, SLOT(issueInstall()));
- if (m_Manager->isInfoIncomplete(m_ContextRow))
- menu.addAction(tr("Query Info"), this, SLOT(issueQueryInfoMd5()));
- else
- menu.addAction(tr("Visit on Nexus"), this, SLOT(issueVisitOnNexus()));
- menu.addAction(tr("Open File"), this, SLOT(issueOpenFile()));
- menu.addAction(tr("Show in Folder"), this, SLOT(issueOpenInDownloadsFolder()));
+ hidden = m_Manager->isHidden(m_ContextRow);
- menu.addSeparator();
+ if (state >= DownloadManager::STATE_READY) {
+ menu.addAction(tr("Install"), this, SLOT(issueInstall()));
+ if (m_Manager->isInfoIncomplete(m_ContextRow))
+ menu.addAction(tr("Query Info"), this, SLOT(issueQueryInfoMd5()));
+ else
+ menu.addAction(tr("Visit on Nexus"), this, SLOT(issueVisitOnNexus()));
+ menu.addAction(tr("Open File"), this, SLOT(issueOpenFile()));
+ menu.addAction(tr("Show in Folder"), this, SLOT(issueOpenInDownloadsFolder()));
- menu.addAction(tr("Delete"), this, SLOT(issueDelete()));
- if (hidden)
- menu.addAction(tr("Un-Hide"), this, SLOT(issueRestoreToView()));
- else
- menu.addAction(tr("Hide"), this, SLOT(issueRemoveFromView()));
- } else if (state == DownloadManager::STATE_DOWNLOADING) {
- menu.addAction(tr("Cancel"), this, SLOT(issueCancel()));
- menu.addAction(tr("Pause"), this, SLOT(issuePause()));
- menu.addAction(tr("Show in Folder"), this, SLOT(issueOpenInDownloadsFolder()));
- } else if ((state == DownloadManager::STATE_PAUSED) || (state == DownloadManager::STATE_ERROR)
- || (state == DownloadManager::STATE_PAUSING)) {
- menu.addAction(tr("Delete"), this, SLOT(issueDelete()));
- menu.addAction(tr("Resume"), this, SLOT(issueResume()));
- menu.addAction(tr("Show in Folder"), this, SLOT(issueOpenInDownloadsFolder()));
- }
+ menu.addSeparator();
+
+ menu.addAction(tr("Delete"), this, SLOT(issueDelete()));
+ if (hidden)
+ menu.addAction(tr("Un-Hide"), this, SLOT(issueRestoreToView()));
+ else
+ menu.addAction(tr("Hide"), this, SLOT(issueRemoveFromView()));
+ } else if (state == DownloadManager::STATE_DOWNLOADING) {
+ menu.addAction(tr("Cancel"), this, SLOT(issueCancel()));
+ menu.addAction(tr("Pause"), this, SLOT(issuePause()));
+ menu.addAction(tr("Show in Folder"), this, SLOT(issueOpenInDownloadsFolder()));
+ } else if ((state == DownloadManager::STATE_PAUSED) || (state == DownloadManager::STATE_ERROR)
+ || (state == DownloadManager::STATE_PAUSING)) {
+ menu.addAction(tr("Delete"), this, SLOT(issueDelete()));
+ menu.addAction(tr("Resume"), this, SLOT(issueResume()));
+ menu.addAction(tr("Show in Folder"), this, SLOT(issueOpenInDownloadsFolder()));
+ }
- menu.addSeparator();
+ menu.addSeparator();
+ }
+ } catch(std::exception&)
+ {
+ // this happens when the download index is not found, ignore it and don't
+ // display download-specific actions
}
+
menu.addAction(tr("Delete Installed Downloads..."), this, SLOT(issueDeleteCompleted()));
menu.addAction(tr("Delete Uninstalled Downloads..."), this, SLOT(issueDeleteUninstalled()));
menu.addAction(tr("Delete All Downloads..."), this, SLOT(issueDeleteAll()));
@@ -277,7 +290,7 @@ void DownloadListWidget::issueDelete()
void DownloadListWidget::issueRemoveFromView()
{
- qDebug() << "removing from view: " << m_ContextRow;
+ log::debug("removing from view: {}", m_ContextRow);
emit removeDownload(m_ContextRow, false);
}
diff --git a/src/downloadmanager.cpp b/src/downloadmanager.cpp
index 648b102a..adfbc84d 100644
--- a/src/downloadmanager.cpp
+++ b/src/downloadmanager.cpp
@@ -200,8 +200,9 @@ QString DownloadManager::DownloadInfo::currentURL()
}
-DownloadManager::DownloadManager(NexusInterface *nexusInterface, QObject *parent)
- : IDownloadManager(parent), m_NexusInterface(nexusInterface), m_DirWatcher(), m_ShowHidden(false)
+DownloadManager::DownloadManager(NexusInterface *nexusInterface, QObject *parent) :
+ IDownloadManager(parent), m_NexusInterface(nexusInterface), m_DirWatcher(), m_ShowHidden(false),
+ m_ParentWidget(nullptr)
{
m_OrganizerCore = dynamic_cast<OrganizerCore*>(parent);
connect(&m_DirWatcher, SIGNAL(directoryChanged(QString)), this, SLOT(directoryChanged(QString)));
@@ -219,6 +220,10 @@ DownloadManager::~DownloadManager()
m_ActiveDownloads.clear();
}
+void DownloadManager::setParentWidget(QWidget* w)
+{
+ m_ParentWidget = w;
+}
bool DownloadManager::downloadsInProgress()
{
@@ -288,12 +293,6 @@ void DownloadManager::setOutputDirectory(const QString &outputDirectory)
}
-void DownloadManager::setPreferredServers(const std::map<QString, int> &preferredServers)
-{
- m_PreferredServers = preferredServers;
-}
-
-
void DownloadManager::setSupportedExtensions(const QStringList &extensions)
{
m_SupportedExtensions = extensions;
@@ -352,7 +351,7 @@ void DownloadManager::refreshList()
}
}
if (orphans.size() > 0) {
- qDebug("%d orphaned meta files will be deleted", orphans.size());
+ log::debug("{} orphaned meta files will be deleted", orphans.size());
shellDelete(orphans, true);
}
@@ -378,9 +377,6 @@ void DownloadManager::refreshList()
}
}
- //if (m_ActiveDownloads.size() != downloadsBefore) {
- qDebug("Downloads after refresh: %d", m_ActiveDownloads.size());
- //}
emit update(-1);
//let watcher trigger refreshes again
@@ -401,7 +397,7 @@ bool DownloadManager::addDownload(const QStringList &URLs, QString gameName,
}
QUrl preferredUrl = QUrl::fromEncoded(URLs.first().toLocal8Bit());
- qDebug("selected download url: %s", qUtf8Printable(preferredUrl.toString()));
+ log::debug("selected download url: {}", preferredUrl.toString());
QNetworkRequest request(preferredUrl);
request.setHeader(QNetworkRequest::UserAgentHeader, m_NexusInterface->getAccessManager()->userAgent());
return addDownload(m_NexusInterface->getAccessManager()->get(request), URLs, fileName, gameName, modID, fileID, fileInfo);
@@ -510,7 +506,7 @@ void DownloadManager::startDownload(QNetworkReply *reply, DownloadInfo *newDownl
if (QFile::exists(m_OutputDirectory + "/" + newDownload->m_FileName)) {
setState(newDownload, STATE_PAUSING);
QCoreApplication::processEvents();
- if (QMessageBox::question(nullptr, tr("Download again?"), tr("A file with the same name \"%1\" has already been downloaded. "
+ if (QMessageBox::question(m_ParentWidget, tr("Download again?"), tr("A file with the same name \"%1\" has already been downloaded. "
"Do you want to download it again? The new file will receive a different name.").arg(newDownload->m_FileName),
QMessageBox::Yes | QMessageBox::No) == QMessageBox::No) {
if (reply->isFinished())
@@ -522,7 +518,7 @@ void DownloadManager::startDownload(QNetworkReply *reply, DownloadInfo *newDownl
newDownload->setName(getDownloadFileName(newDownload->m_FileName, true), true);
endDisableDirWatcher();
if (newDownload->m_State == STATE_PAUSED)
- resumeDownload(indexByName(newDownload->m_FileName));
+ resumeDownload(indexByInfo(newDownload));
else
setState(newDownload, STATE_DOWNLOADING);
}
@@ -536,7 +532,7 @@ void DownloadManager::startDownload(QNetworkReply *reply, DownloadInfo *newDownl
newDownload->m_State != STATE_READY &&
newDownload->m_State != STATE_FETCHINGMODINFO &&
reply->isFinished()) {
- downloadFinished(indexByName(newDownload->m_FileName));
+ downloadFinished(indexByInfo(newDownload));
return;
}
} else
@@ -562,24 +558,25 @@ void DownloadManager::addNXMDownload(const QString &url)
break;
}
}
- qDebug("add nxm download: %s", qUtf8Printable(url));
+ log::debug("add nxm download: {}", url);
if (foundGame == nullptr) {
- qDebug("download requested for wrong game (game: %s, url: %s)", qUtf8Printable(m_ManagedGame->gameShortName()), qUtf8Printable(nxmInfo.game()));
- QMessageBox::information(nullptr, tr("Wrong Game"), tr("The download link is for a mod for \"%1\" but this instance of MO "
+ log::debug("download requested for wrong game (game: {}, url: {})", m_ManagedGame->gameShortName(), nxmInfo.game());
+ QMessageBox::information(m_ParentWidget, tr("Wrong Game"), tr("The download link is for a mod for \"%1\" but this instance of MO "
"has been set up for \"%2\".").arg(nxmInfo.game()).arg(m_ManagedGame->gameShortName()), QMessageBox::Ok);
return;
}
for (auto tuple : m_PendingDownloads) {
if (std::get<0>(tuple).compare(foundGame->gameShortName(), Qt::CaseInsensitive) == 0, std::get<1>(tuple) == nxmInfo.modId() && std::get<2>(tuple) == nxmInfo.fileId()) {
- QString debugStr("download requested is already queued (mod: %1, file: %2)");
- QString infoStr(tr("There is already a download queued for this file.\n\nMod %1\nFile %2"));
+ const auto infoStr =
+ tr("There is already a download queued for this file.\n\nMod %1\nFile %2")
+ .arg(nxmInfo.modId()).arg(nxmInfo.fileId());
- debugStr = debugStr.arg(nxmInfo.modId()).arg(nxmInfo.fileId());
- infoStr = infoStr.arg(nxmInfo.modId()).arg(nxmInfo.fileId());
+ log::debug(
+ "download requested is already queued (mod: {}, file: {})",
+ nxmInfo.modId(), nxmInfo.fileId());
- qDebug(qUtf8Printable(debugStr));
- QMessageBox::information(nullptr, tr("Already Queued"), infoStr, QMessageBox::Ok);
+ QMessageBox::information(m_ParentWidget, tr("Already Queued"), infoStr, QMessageBox::Ok);
return;
}
}
@@ -622,8 +619,8 @@ void DownloadManager::addNXMDownload(const QString &url)
infoStr = infoStr.arg(QStringLiteral("<blank>"));
}
- qDebug(qUtf8Printable(debugStr));
- QMessageBox::information(nullptr, tr("Already Started"), infoStr, QMessageBox::Ok);
+ log::debug("{}", debugStr);
+ QMessageBox::information(m_ParentWidget, tr("Already Started"), infoStr, QMessageBox::Ok);
return;
}
}
@@ -660,7 +657,7 @@ void DownloadManager::removeFile(int index, bool deleteFile)
if ((download->m_State == STATE_STARTED) ||
(download->m_State == STATE_DOWNLOADING)) {
// shouldn't have been possible
- qCritical("tried to remove active download");
+ log::error("tried to remove active download");
endDisableDirWatcher();
return;
}
@@ -717,7 +714,7 @@ void DownloadManager::refreshAlphabeticalTranslation()
m_AlphabeticalTranslation.push_back(pos);
}
- qSort(m_AlphabeticalTranslation.begin(), m_AlphabeticalTranslation.end(), LessThanWrapper(this));
+ std::sort(m_AlphabeticalTranslation.begin(), m_AlphabeticalTranslation.end(), LessThanWrapper(this));
}
@@ -798,7 +795,7 @@ void DownloadManager::removeDownload(int index, bool deleteFile)
emit update(-1);
endDisableDirWatcher();
} catch (const std::exception &e) {
- qCritical("failed to remove download: %s", e.what());
+ log::error("failed to remove download: {}", e.what());
}
refreshList();
}
@@ -883,7 +880,7 @@ void DownloadManager::resumeDownloadInt(int index)
if (info->m_State == STATE_ERROR) {
info->m_CurrentUrl = (info->m_CurrentUrl + 1) % info->m_Urls.count();
}
- qDebug("request resume from url %s", qUtf8Printable(info->currentURL()));
+ log::debug("request resume from url {}", info->currentURL());
QNetworkRequest request(QUrl::fromEncoded(info->currentURL().toLocal8Bit()));
request.setHeader(QNetworkRequest::UserAgentHeader, m_NexusInterface->getAccessManager()->userAgent());
if (info->m_State != STATE_ERROR) {
@@ -896,7 +893,7 @@ void DownloadManager::resumeDownloadInt(int index)
std::get<2>(info->m_SpeedDiff) = 0;
std::get<3>(info->m_SpeedDiff) = 0;
std::get<4>(info->m_SpeedDiff) = 0;
- qDebug("resume at %lld bytes", info->m_ResumePos);
+ log::debug("resume at {} bytes", info->m_ResumePos);
startDownload(m_NexusInterface->getAccessManager()->get(request), info, true);
}
emit update(index);
@@ -924,7 +921,7 @@ void DownloadManager::queryInfo(int index)
DownloadInfo *info = m_ActiveDownloads[index];
if (info->m_FileInfo->repository != "Nexus") {
- qWarning("re-querying file info is currently only possible with Nexus");
+ log::warn("re-querying file info is currently only possible with Nexus");
return;
}
@@ -976,7 +973,7 @@ void DownloadManager::queryInfoMd5(int index)
DownloadInfo *info = m_ActiveDownloads[index];
if (info->m_FileInfo->repository != "Nexus") {
- qWarning("re-querying file info is currently only possible with Nexus");
+ log::warn("re-querying file info is currently only possible with Nexus");
return;
}
@@ -993,16 +990,39 @@ void DownloadManager::queryInfoMd5(int index)
downloadFile.setFileName(m_OrganizerCore->downloadsPath() + "\\" + info->m_FileName);
}
if (!downloadFile.exists()) {
- qDebug("Can't find download file %s", info->m_FileName);
+ log::error("Can't find download file '{}'", info->m_FileName);
return;
}
if (!downloadFile.open(QIODevice::ReadOnly)) {
- qDebug("Can't open download file %s", info->m_FileName);
+ log::error("Can't open download file '{}'", info->m_FileName);
return;
}
- info->m_Hash = QCryptographicHash::hash(downloadFile.readAll(), QCryptographicHash::Md5);
+
+ QCryptographicHash hash(QCryptographicHash::Md5);
+ const qint64 progressStep = 10 * 1024 * 1024;
+ QProgressDialog progress(tr("Hashing download file '%1'").arg(info->m_FileName),
+ tr("Cancel"),
+ 0,
+ downloadFile.size() / progressStep);
+ progress.setWindowModality(Qt::WindowModal);
+ progress.setMinimumDuration(1000);
+
+ for (qint64 i = 0; i < downloadFile.size(); i += progressStep) {
+ progress.setValue(progress.value()+1);
+ if (progress.wasCanceled()) {
+ break;
+ }
+ hash.addData(downloadFile.read(progressStep));
+ }
+ if (progress.wasCanceled()) {
+ downloadFile.close();
+ return;
+ }
+
+ progress.close();
downloadFile.close();
+ info->m_Hash = hash.result();
info->m_ReQueried = true;
setState(info, STATE_FETCHINGMODINFO_MD5);
}
@@ -1016,7 +1036,7 @@ void DownloadManager::visitOnNexus(int index)
DownloadInfo *info = m_ActiveDownloads[index];
if (info->m_FileInfo->repository != "Nexus") {
- qWarning("Visiting mod page is currently only possible with Nexus");
+ log::warn("Visiting mod page is currently only possible with Nexus");
return;
}
@@ -1044,11 +1064,11 @@ void DownloadManager::openFile(int index)
QDir path = QDir(m_OutputDirectory);
if (path.exists(getFileName(index))) {
- shell::OpenFile(getFilePath(index));
+ shell::Open(getFilePath(index));
return;
}
- shell::ExploreFile(m_OutputDirectory);
+ shell::Explore(m_OutputDirectory);
return;
}
@@ -1062,18 +1082,18 @@ void DownloadManager::openInDownloadsFolder(int index)
const auto path = getFilePath(index);
if (QFile::exists(path)) {
- shell::ExploreFile(path);
+ shell::Explore(path);
return;
}
else {
const auto unfinished = path + ".unfinished";
if (QFile::exists(unfinished)) {
- shell::ExploreFile(unfinished);
+ shell::Explore(unfinished);
return;
}
}
- shell::ExploreFile(m_OutputDirectory);
+ shell::Explore(m_OutputDirectory);
}
@@ -1155,7 +1175,12 @@ QDateTime DownloadManager::getFileTime(int index) const
DownloadInfo *info = m_ActiveDownloads.at(index);
if (!info->m_Created.isValid()) {
- info->m_Created = QFileInfo(info->m_Output).created();
+ QFileInfo fileInfo(info->m_Output);
+ info->m_Created = fileInfo.birthTime();
+ if (!info->m_Created.isValid())
+ info->m_Created = fileInfo.metadataChangeTime();
+ if (!info->m_Created.isValid())
+ info->m_Created = fileInfo.lastModified();
}
return info->m_Created;
@@ -1214,6 +1239,19 @@ int DownloadManager::getModID(int index) const
return m_ActiveDownloads.at(index)->m_FileInfo->modID;
}
+QString DownloadManager::getDisplayGameName(int index) const
+{
+ if ((index < 0) || (index >= m_ActiveDownloads.size())) {
+ throw MyException(tr("mod id: invalid download index %1").arg(index));
+ }
+ QString gameName = m_ActiveDownloads.at(index)->m_FileInfo->gameName;
+ IPluginGame* gamePlugin = m_OrganizerCore->getGame(gameName);
+ if (gamePlugin) {
+ gameName = gamePlugin->gameName();
+ }
+ return gameName;
+}
+
QString DownloadManager::getGameName(int index) const
{
if ((index < 0) || (index >= m_ActiveDownloads.size())) {
@@ -1384,7 +1422,7 @@ void DownloadManager::setState(DownloadManager::DownloadInfo *info, DownloadMana
m_RequestIDs.insert(m_NexusInterface->requestFiles(info->m_FileInfo->gameName, info->m_FileInfo->modID, this, info->m_DownloadID, QString()));
} break;
case STATE_FETCHINGMODINFO_MD5: {
- qDebug(qUtf8Printable(QString("Searching %1 for MD5 of %2").arg(info->m_GamesToQuery[0]).arg(QString(info->m_Hash.toHex()))));
+ log::debug("Searching {} for MD5 of {}", info->m_GamesToQuery[0], QString(info->m_Hash.toHex()));
m_RequestIDs.insert(m_NexusInterface->requestInfoFromMd5(info->m_GamesToQuery[0], info->m_Hash, this, info->m_DownloadID, QString()));
} break;
case STATE_READY: {
@@ -1444,22 +1482,11 @@ void DownloadManager::downloadProgress(qint64 bytesReceived, qint64 bytesTotal)
std::get<4>(info->m_SpeedDiff) = ((calc*0.5) + (std::get<4>(info->m_SpeedDiff)*1.5)) / 2;
// calculate the download speed
- double speed = (std::get<4>(info->m_SpeedDiff) * 1000.0) / (5 * 1000);
+ const double speed = (std::get<4>(info->m_SpeedDiff) * 1000.0) / (5 * 1000);
- QString unit;
- if (speed < 1000) {
- unit = "B/s";
- }
- else if (speed < 1000*1024) {
- speed /= 1024;
- unit = "KB/s";
- }
- else {
- speed /= 1024 * 1024;
- unit = "MB/s";
- }
-
- info->m_Progress.second = QString::fromLatin1("%1% - %2 %3").arg(info->m_Progress.first).arg(QString::number(speed, 'f', 1)).arg(unit);
+ info->m_Progress.second = QString::fromLatin1("%1% - %2")
+ .arg(info->m_Progress.first)
+ .arg(MOBase::localizedByteSpeed(speed));
TaskProgressManager::instance().updateProgress(info->m_TaskProgressId, bytesReceived, bytesTotal);
emit update(index);
@@ -1597,6 +1624,8 @@ void DownloadManager::nxmFilesAvailable(QString, int, QVariant userData, QVarian
emit showMessage(tr("No matching file found on Nexus! Maybe this file is no longer available or it was renamed?"));
} else {
SelectionDialog selection(tr("No file on Nexus matches the selected file by name. Please manually choose the correct one."));
+ std::sort(files.begin(), files.end(), [](const QVariant& lhs, const QVariant& rhs)
+ {return lhs.toMap()["uploaded_timestamp"].toInt() > rhs.toMap()["uploaded_timestamp"].toInt();});
for (QVariant file : files) {
QVariantMap fileInfo = file.toMap();
if (fileInfo["category_id"].toInt() != 6)
@@ -1614,8 +1643,9 @@ void DownloadManager::nxmFilesAvailable(QString, int, QVariant userData, QVarian
}
} else {
if (info->m_FileInfo->fileID == 0) {
- qWarning("could not determine file id for %s (state %d)",
- qUtf8Printable(info->m_FileName), info->m_State);
+ log::warn(
+ "could not determine file id for {} (state {})",
+ info->m_FileName, info->m_State);
}
}
@@ -1663,23 +1693,38 @@ void DownloadManager::nxmFileInfoAvailable(QString gameName, int modID, int file
m_RequestIDs.insert(m_NexusInterface->requestDownloadURL(info->gameName, info->modID, info->fileID, this, qVariantFromValue(test), QString()));
}
-static int evaluateFileInfoMap(const QVariantMap &map, const std::map<QString, int> &preferredServers)
+static int evaluateFileInfoMap(
+ const QVariantMap &map,
+ const ServerList::container& preferredServers)
{
- int result = 0;
+ int preference = 0;
+ bool found = false;
+ const auto name = map["short_name"].toString();
- auto preference = preferredServers.find(map["short_name"].toString());
+ for (const auto& server : preferredServers) {
+ if (server.name() == name) {
+ preference = server.preferred();
+ found = true;
+ break;
+ }
+ }
- if (preference != preferredServers.end()) {
- result += 100 + preference->second * 20;
+ if (!found) {
+ return 0;
}
- return result;
+ return 100 + preference * 20;
}
// sort function to sort by best download server
-bool DownloadManager::ServerByPreference(const std::map<QString, int> &preferredServers, const QVariant &LHS, const QVariant &RHS)
+//
+bool ServerByPreference(
+ const ServerList::container& preferredServers,
+ const QVariant &LHS, const QVariant &RHS)
{
- return evaluateFileInfoMap(LHS.toMap(), preferredServers) > evaluateFileInfoMap(RHS.toMap(), preferredServers);
+ const auto a = evaluateFileInfoMap(LHS.toMap(), preferredServers);
+ const auto b = evaluateFileInfoMap(RHS.toMap(), preferredServers);
+ return (a > b);
}
int DownloadManager::startDownloadURLs(const QStringList &urls)
@@ -1711,6 +1756,16 @@ int DownloadManager::indexByName(const QString &fileName) const
return -1;
}
+int DownloadManager::indexByInfo(const DownloadInfo* info) const
+{
+ for (int i = 0; i < m_ActiveDownloads.size(); ++i) {
+ if (m_ActiveDownloads[i] == info) {
+ return i;
+ }
+ }
+ return -1;
+}
+
void DownloadManager::nxmDownloadURLsAvailable(QString gameName, int modID, int fileID, QVariant userData, QVariant resultData, int requestID)
{
std::set<int>::iterator idIter = m_RequestIDs.find(requestID);
@@ -1728,7 +1783,12 @@ void DownloadManager::nxmDownloadURLsAvailable(QString gameName, int modID, int
return;
}
- std::sort(resultList.begin(), resultList.end(), boost::bind(&DownloadManager::ServerByPreference, m_PreferredServers, _1, _2));
+ const auto servers = m_OrganizerCore->settings().network().servers();
+
+ std::sort(
+ resultList.begin(),
+ resultList.end(),
+ boost::bind(&ServerByPreference, servers.getPreferred(), _1, _2));
info->userData["downloadMap"] = resultList;
@@ -1779,7 +1839,7 @@ void DownloadManager::nxmFileInfoFromMd5Available(QString gameName, QVariant use
if (chosenIdx < 0) {
chosenIdx = i; //intentional to not break in order to check other results
} else {
- qDebug("Multiple active files found during MD5 search. Defaulting to time stamps...");
+ log::debug("Multiple active files found during MD5 search. Defaulting to time stamps...");
chosenIdx = -1;
break;
}
@@ -1999,7 +2059,7 @@ void DownloadManager::downloadFinished(int index)
resumeDownloadInt(index);
}
} else {
- qWarning("no download index %d", index);
+ log::warn("no download index {}", index);
}
}
@@ -2008,9 +2068,9 @@ void DownloadManager::downloadError(QNetworkReply::NetworkError error)
{
if (error != QNetworkReply::OperationCanceledError) {
QNetworkReply *reply = qobject_cast<QNetworkReply*>(sender());
- qWarning("%s (%d)", reply != nullptr ? qUtf8Printable(reply->errorString())
- : "Download error occured",
- error);
+ log::warn("{} ({})",
+ reply != nullptr ? reply->errorString() : "Download error occured",
+ error);
}
}
@@ -2033,7 +2093,7 @@ void DownloadManager::metaDataChanged()
}
}
} else {
- qWarning("meta data event for unknown download");
+ log::warn("meta data event for unknown download");
}
}
@@ -2068,7 +2128,11 @@ void DownloadManager::writeData(DownloadInfo *info)
if (ret < info->m_Reply->size()) {
QString fileName = info->m_FileName; // m_FileName may be destroyed after setState
setState(info, DownloadState::STATE_CANCELED);
- qCritical(QString("Unable to write download \"%2\" to drive (return %1)").arg(ret).arg(info->m_FileName).toLocal8Bit());
+
+ log::error(
+ "Unable to write download \"{}\" to drive (return {})",
+ info->m_FileName, ret);
+
reportError(tr("Unable to write download to drive (return %1).\n"
"Check the drive's available storage.\n\n"
"Canceling download \"%2\"...").arg(ret).arg(fileName));
diff --git a/src/downloadmanager.h b/src/downloadmanager.h
index feef0eaa..4fc61cad 100644
--- a/src/downloadmanager.h
+++ b/src/downloadmanager.h
@@ -20,6 +20,7 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#ifndef DOWNLOADMANAGER_H
#define DOWNLOADMANAGER_H
+#include "serverinfo.h"
#include <idownloadmanager.h>
#include <modrepositoryfileinfo.h>
#include <set>
@@ -136,6 +137,8 @@ public:
~DownloadManager();
+ void setParentWidget(QWidget* w);
+
/**
* @brief determine if a download is currently in progress
*
@@ -174,11 +177,6 @@ public:
QString getOutputDirectory() const { return m_OutputDirectory; }
/**
- * @brief setPreferredServers set the list of preferred servers
- */
- void setPreferredServers(const std::map<QString, int> &preferredServers);
-
- /**
* @brief set the list of supported extensions
* @param extensions list of supported extensions
*/
@@ -315,6 +313,14 @@ public:
int getModID(int index) const;
/**
+ * @brief retrieve the displayable game name of the download specified by the index
+ *
+ * @param index index of the file to look up
+ * @return the displayable game name
+ **/
+ QString getDisplayGameName(int index) const;
+
+ /**
* @brief retrieve the game name of the downlaod specified by the index
*
* @param index index of the file to look up
@@ -360,15 +366,6 @@ public:
*/
void refreshList();
- /**
- * @brief Sort function for download servers
- * @param LHS
- * @param RHS
- * @return
- */
- static bool ServerByPreference(const std::map<QString, int> &preferredServers, const QVariant &LHS, const QVariant &RHS);
-
-
virtual int startDownloadURLs(const QStringList &urls);
virtual int startDownloadNexusFile(int modID, int fileID);
@@ -381,6 +378,7 @@ public:
* @return index of that download or -1 if it wasn't found
*/
int indexByName(const QString &fileName) const;
+ int indexByInfo(const DownloadInfo* info) const;
void pauseAll();
@@ -542,13 +540,13 @@ private:
NexusInterface *m_NexusInterface;
OrganizerCore *m_OrganizerCore;
+ QWidget* m_ParentWidget;
QVector<std::tuple<QString, int, int>> m_PendingDownloads;
QVector<DownloadInfo*> m_ActiveDownloads;
QString m_OutputDirectory;
- std::map<QString, int> m_PreferredServers;
QStringList m_SupportedExtensions;
std::set<int> m_RequestIDs;
QVector<int> m_AlphabeticalTranslation;
diff --git a/src/editexecutablesdialog.cpp b/src/editexecutablesdialog.cpp
index 8929d207..1c804a7c 100644
--- a/src/editexecutablesdialog.cpp
+++ b/src/editexecutablesdialog.cpp
@@ -24,6 +24,7 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#include "modlist.h"
#include "forcedloaddialog.h"
#include "organizercore.h"
+#include "spawn.h"
#include <QMessageBox>
#include <Shellapi.h>
@@ -33,7 +34,29 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
using namespace MOBase;
using namespace MOShared;
-EditExecutablesDialog::EditExecutablesDialog(OrganizerCore& oc, QWidget* parent)
+class IgnoreChanges
+{
+public:
+ IgnoreChanges(EditExecutablesDialog* d)
+ : m_dialog(d)
+ {
+ m_dialog->m_settingUI = true;
+ }
+
+ ~IgnoreChanges()
+ {
+ m_dialog->m_settingUI = false;
+ }
+
+ IgnoreChanges(const IgnoreChanges&) = delete;
+ IgnoreChanges& operator=(const IgnoreChanges&) = delete;
+
+private:
+ EditExecutablesDialog* m_dialog;
+};
+
+
+EditExecutablesDialog::EditExecutablesDialog(OrganizerCore& oc, int sel, QWidget* parent)
: TutorableDialog("EditExecutables", parent)
, ui(new Ui::EditExecutablesDialog)
, m_organizerCore(oc)
@@ -49,10 +72,25 @@ EditExecutablesDialog::EditExecutablesDialog(OrganizerCore& oc, QWidget* parent)
loadCustomOverwrites();
loadForcedLibraries();
- ui->mods->addItems(m_organizerCore.modList()->allMods());
+ for (auto&& m : m_organizerCore.modList()->allMods()) {
+ if (ModInfo::isRegularName(m)) {
+ ui->mods->addItem(m);
+ }
+ }
+
fillList();
setDirty(false);
+ if (sel >= 0 && sel < ui->list->count()) {
+ selectIndex(sel);
+ }
+
+ auto* m = new QMenu;
+ m->addAction(tr("Add from file..."), [&]{ addFromFile(); });
+ m->addAction(tr("Add empty"), [&]{ addEmpty(); });
+ m->addAction(tr("Clone selected"), [&]{ clone(); });
+ ui->add->setMenu(m);
+
// some widgets need to do more than just save() and have their own handler
connect(ui->binary, &QLineEdit::textChanged, [&]{ save(); });
connect(ui->workingDirectory, &QLineEdit::textChanged, [&]{ save(); });
@@ -60,11 +98,17 @@ EditExecutablesDialog::EditExecutablesDialog(OrganizerCore& oc, QWidget* parent)
connect(ui->steamAppID, &QLineEdit::textChanged, [&]{ save(); });
connect(ui->mods, &QComboBox::currentTextChanged, [&]{ save(); });
connect(ui->useApplicationIcon, &QCheckBox::toggled, [&]{ save(); });
+ connect(ui->hide, &QCheckBox::toggled, [&]{ save(); });
connect(ui->list->model(), &QAbstractItemModel::rowsMoved, [&]{ saveOrder(); });
}
EditExecutablesDialog::~EditExecutablesDialog() = default;
+int EditExecutablesDialog::exec()
+{
+ GeometrySaver gs(Settings::instance(), this);
+ return QDialog::exec();
+}
void EditExecutablesDialog::loadCustomOverwrites()
{
@@ -101,9 +145,7 @@ ExecutablesList EditExecutablesDialog::getExecutablesList() const
auto itor = m_executablesList.find(title);
if (itor == m_executablesList.end()) {
- qWarning().nospace()
- << "getExecutablesList(): executable '" << title << "' not found";
-
+ log::warn("getExecutablesList(): executable '{}' not found", title);
continue;
}
@@ -165,6 +207,14 @@ void EditExecutablesDialog::setDirty(bool b)
}
}
+void EditExecutablesDialog::selectIndex(int i)
+{
+ if (i >= 0 && i < ui->list->count()) {
+ ui->list->selectionModel()->setCurrentIndex(
+ ui->list->model()->index(i, 0), QItemSelectionModel::ClearAndSelect);
+ }
+}
+
QListWidgetItem* EditExecutablesDialog::selectedItem()
{
const auto selection = ui->list->selectedItems();
@@ -203,7 +253,7 @@ void EditExecutablesDialog::fillList()
// select the first one in the list, if any
if (ui->list->count() > 0) {
- ui->list->item(0)->setSelected(true);
+ selectIndex(0);
} else {
updateUI(nullptr, nullptr);
}
@@ -218,7 +268,7 @@ void EditExecutablesDialog::updateUI(
const QListWidgetItem* item, const Executable* e)
{
// the ui is currently being set, ignore changes
- m_settingUI = true;
+ IgnoreChanges c(this);
if (e) {
setEdits(*e);
@@ -227,9 +277,6 @@ void EditExecutablesDialog::updateUI(
}
setButtons(item, e);
-
- // any changes from now on are from the user
- m_settingUI = false;
}
void EditExecutablesDialog::setButtons(
@@ -271,18 +318,25 @@ void EditExecutablesDialog::clearEdits()
ui->configureLibraries->setEnabled(false);
ui->useApplicationIcon->setEnabled(false);
ui->useApplicationIcon->setChecked(false);
+ ui->hide->setEnabled(false);
+ ui->hide->setChecked(false);
+
+ m_lastGoodTitle = "";
}
void EditExecutablesDialog::setEdits(const Executable& e)
{
ui->title->setText(e.title());
- ui->binary->setText(QDir::toNativeSeparators(e.binaryInfo().absoluteFilePath()));
+ ui->binary->setText(QDir::toNativeSeparators(e.binaryInfo().filePath()));
ui->workingDirectory->setText(QDir::toNativeSeparators(e.workingDirectory()));
ui->arguments->setText(e.arguments());
ui->overwriteSteamAppID->setChecked(!e.steamAppID().isEmpty());
ui->steamAppID->setEnabled(!e.steamAppID().isEmpty());
ui->steamAppID->setText(e.steamAppID());
ui->useApplicationIcon->setChecked(e.usesOwnIcon());
+ ui->hide->setChecked(e.hide());
+
+ m_lastGoodTitle = e.title();
{
int modIndex = -1;
@@ -293,9 +347,10 @@ void EditExecutablesDialog::setEdits(const Executable& e)
modIndex = ui->mods->findText(modName->value);
if (modIndex == -1) {
- qWarning().nospace()
- << "executable '" << e.title() << "' uses mod '" << modName->value << "' "
- << "as a custom overwrite, but that mod doesn't exist";
+ log::warn(
+ "executable '{}' uses mod '{}' as a custom overwrite, but that mod "
+ "doesn't exist",
+ e.title(), modName->value);
}
}
@@ -325,6 +380,7 @@ void EditExecutablesDialog::setEdits(const Executable& e)
ui->useApplicationIcon->setEnabled(true);
ui->createFilesInMod->setEnabled(true);
ui->forceLoadLibraries->setEnabled(true);
+ ui->hide->setEnabled(true);
}
void EditExecutablesDialog::save()
@@ -335,7 +391,7 @@ void EditExecutablesDialog::save()
auto* e = selectedExe();
if (!e) {
- qWarning("trying to save but nothing is selected");
+ log::warn("trying to save but nothing is selected");
return;
}
@@ -353,7 +409,7 @@ void EditExecutablesDialog::save()
// get the new title, but ignore it if it's conflicting with an already
// existing executable
- QString newTitle = ui->title->text();
+ QString newTitle = ui->title->text().trimmed();
if (isTitleConflicting(newTitle)) {
newTitle = e->title();
}
@@ -384,6 +440,12 @@ void EditExecutablesDialog::save()
e->flags(e->flags() & (~Executable::UseApplicationIcon));
}
+ if (ui->hide->isChecked()) {
+ e->flags(e->flags() | Executable::Hide);
+ } else {
+ e->flags(e->flags() & (~Executable::Hide));
+ }
+
setDirty(true);
}
@@ -417,13 +479,14 @@ void EditExecutablesDialog::move(QListWidgetItem* item, int direction)
return;
}
- const auto row = ui->list->row(item);
+ const auto oldRow = ui->list->row(item);
+ const auto newRow = oldRow + (direction > 0 ? 1 : -1);
// removing item
- ui->list->takeItem(row);
- ui->list->insertItem(row + (direction > 0 ? 1 : -1), item);
- item->setSelected(true);
+ ui->list->takeItem(oldRow);
+ ui->list->insertItem(newRow, item);
+ selectIndex(newRow);
setDirty(true);
}
@@ -455,33 +518,20 @@ void EditExecutablesDialog::on_reset_clicked()
void EditExecutablesDialog::on_add_clicked()
{
- auto title = m_executablesList.makeNonConflictingTitle(tr("New Executable"));
- if (!title) {
- return;
- }
-
- const Executable e(*title);
-
- m_executablesList.setExecutable(e);
-
- auto* item = createListItem(e);
- ui->list->addItem(item);
- item->setSelected(true);
-
- setDirty(true);
+ addFromFile();
}
void EditExecutablesDialog::on_remove_clicked()
{
auto* item = selectedItem();
if (!item) {
- qWarning("trying to remove entry but nothing is selected");
+ log::warn("trying to remove entry but nothing is selected");
return;
}
auto* exe = selectedExe();
if (!exe) {
- qWarning("trying to remove entry but nothing is selected");
+ log::warn("trying to remove entry but nothing is selected");
return;
}
@@ -501,10 +551,10 @@ void EditExecutablesDialog::on_remove_clicked()
if (currentRow >= ui->list->count()) {
// that was the last item, select the new list item, if any
if (ui->list->count() > 0) {
- ui->list->item(ui->list->count() - 1)->setSelected(true);
+ selectIndex(ui->list->count() - 1);
}
} else {
- ui->list->item(currentRow)->setSelected(true);
+ selectIndex(currentRow);
}
setDirty(true);
@@ -544,17 +594,26 @@ bool EditExecutablesDialog::isTitleConflicting(const QString& s)
return false;
}
-void EditExecutablesDialog::on_title_textChanged(const QString& s)
+void EditExecutablesDialog::on_title_textChanged(const QString& original)
{
if (m_settingUI) {
return;
}
- // don't allow changing the title to something that already exists
+ auto s = original.trimmed();
+
+ // disallow empty names
+ if (s.isEmpty()) {
+ return;
+ }
+
+ // disallow changing the title to something that already exists
if (isTitleConflicting(s)) {
return;
}
+ m_lastGoodTitle = s;
+
// must save before modifying the item in the list widget because saving
// relies on the item's text being the same as an item in m_executablesList
save();
@@ -566,6 +625,11 @@ void EditExecutablesDialog::on_title_textChanged(const QString& s)
}
}
+void EditExecutablesDialog::on_title_editingFinished()
+{
+ ui->title->setText(m_lastGoodTitle);
+}
+
void EditExecutablesDialog::on_overwriteSteamAppID_toggled(bool checked)
{
if (m_settingUI) {
@@ -598,34 +662,78 @@ void EditExecutablesDialog::on_forceLoadLibraries_toggled(bool checked)
void EditExecutablesDialog::on_browseBinary_clicked()
{
- const QString binaryName = FileDialogMemory::getOpenFileName(
- "editExecutableBinary", this, tr("Select a binary"), ui->binary->text(),
- tr("Executable (%1)").arg("*.exe *.bat *.jar"));
+ const auto binaryName = browseBinary(ui->binary->text());
+ if (binaryName.fileName().isEmpty()) {
+ return;
+ }
+
+ setBinary(binaryName);
+ save();
+}
- if (binaryName.isNull()) {
- // canceled
+void EditExecutablesDialog::addFromFile()
+{
+ const auto binary = browseBinary(ui->binary->text());
+ if (binary.fileName().isEmpty()) {
return;
}
+ addNew(Executable(binary.completeBaseName()));
+ setBinary(binary);
+}
+
+void EditExecutablesDialog::addEmpty()
+{
+ addNew(Executable(tr("New Executable")));
+}
+
+void EditExecutablesDialog::clone()
+{
+ auto* e = selectedExe();
+ if (!e) {
+ return;
+ }
+
+ addNew(*e);
+}
+
+void EditExecutablesDialog::addNew(Executable e)
+{
+ const auto fixedTitle = m_executablesList.makeNonConflictingTitle(e.title());
+ if (!fixedTitle) {
+ return;
+ }
+
+ e.title(*fixedTitle);
+
+ m_executablesList.setExecutable(e);
+
+ auto* item = createListItem(e);
+ ui->list->addItem(item);
+
+ selectIndex(ui->list->count() - 1);
+ setDirty(true);
+}
+
+void EditExecutablesDialog::setBinary(const QFileInfo& binary)
+{
// setting binary
- if (binaryName.endsWith(".jar", Qt::CaseInsensitive)) {
+ if (binary.suffix().compare("jar", Qt::CaseInsensitive) == 0) {
// special case for jar files, uses the system java installation
- setJarBinary(binaryName);
+ setJarBinary(binary);
} else {
- ui->binary->setText(QDir::toNativeSeparators(binaryName));
+ ui->binary->setText(QDir::toNativeSeparators(binary.absoluteFilePath()));
}
- // setting title if currently empty
- if (ui->title->text().isEmpty()) {
- const auto prefix = QFileInfo(binaryName).baseName();
+ // setting title if some variation of "New Executable"
+ if (ui->title->text().startsWith(tr("New Executable"), Qt::CaseInsensitive)) {
+ const auto prefix = binary.completeBaseName();
const auto newTitle = m_executablesList.makeNonConflictingTitle(prefix);
if (newTitle) {
ui->title->setText(*newTitle);
}
}
-
- save();
}
void EditExecutablesDialog::on_browseWorkingDirectory_clicked()
@@ -635,7 +743,7 @@ void EditExecutablesDialog::on_browseWorkingDirectory_clicked()
ui->workingDirectory->text());
if (dirName.isNull()) {
- // canceled
+ // cancelled
return;
}
@@ -646,7 +754,7 @@ void EditExecutablesDialog::on_configureLibraries_clicked()
{
auto* e = selectedExe();
if (!e) {
- qWarning("trying to configure libraries but nothing is selected");
+ log::warn("trying to configure libraries but nothing is selected");
return;
}
@@ -674,25 +782,43 @@ void EditExecutablesDialog::on_buttons_clicked(QAbstractButton* b)
}
}
-void EditExecutablesDialog::setJarBinary(const QString& binaryName)
+QFileInfo EditExecutablesDialog::browseBinary(const QString& initial)
{
- auto java = OrganizerCore::findJavaInstallation(binaryName);
+ const QString Filters =
+ tr("Executables (*.exe *.bat *.jar)") + ";;" +
+ tr("All Files (*.*)");
+
+ const auto f = FileDialogMemory::getOpenFileName(
+ "editExecutableBinary", this, tr("Select an executable"),
+ initial, Filters);
+
+ if (f.isNull()) {
+ return {};
+ }
+
+ return QFileInfo(f);
+}
+
+void EditExecutablesDialog::setJarBinary(const QFileInfo& binary)
+{
+ auto java = spawn::findJavaInstallation(binary.absoluteFilePath());
if (java.isEmpty()) {
QMessageBox::information(
- this, tr("Java (32-bit) required"),
- tr("MO requires 32-bit java to run this application. If you already "
+ this, tr("Java required"),
+ tr("MO requires Java to run this application. If you already "
"have it installed, select javaw.exe from that installation as "
"the binary."));
}
- // only save once
+ {
+ // only save once
+ IgnoreChanges c(this);
- m_settingUI = true;
- ui->binary->setText(java);
- ui->workingDirectory->setText(QDir::toNativeSeparators(QFileInfo(binaryName).absolutePath()));
- ui->arguments->setText("-jar \"" + QDir::toNativeSeparators(binaryName) + "\"");
- m_settingUI = false;
+ ui->binary->setText(java);
+ ui->workingDirectory->setText(QDir::toNativeSeparators(binary.absolutePath()));
+ ui->arguments->setText("-jar \"" + QDir::toNativeSeparators(binary.absoluteFilePath()) + "\"");
+ }
save();
}
diff --git a/src/editexecutablesdialog.h b/src/editexecutablesdialog.h
index 9715489e..c8be0bf6 100644
--- a/src/editexecutablesdialog.h
+++ b/src/editexecutablesdialog.h
@@ -141,16 +141,22 @@ private:
**/
class EditExecutablesDialog : public MOBase::TutorableDialog
{
- Q_OBJECT
+ Q_OBJECT;
+ friend class IgnoreChanges;
public:
using CustomOverwrites = ToggableMap<QString>;
using ForcedLibraries = ToggableMap<QList<MOBase::ExecutableForcedLoadSetting>>;
- explicit EditExecutablesDialog(OrganizerCore& oc, QWidget* parent=nullptr);
+ explicit EditExecutablesDialog(
+ OrganizerCore& oc, int selection=-1, QWidget* parent=nullptr);
~EditExecutablesDialog();
+ // also saves and restores geometry
+ //
+ int exec() override;
+
ExecutablesList getExecutablesList() const;
const CustomOverwrites& getCustomOverwrites() const;
const ForcedLibraries& getForcedLibraries() const;
@@ -165,6 +171,7 @@ private slots:
void on_down_clicked();
void on_title_textChanged(const QString& s);
+ void on_title_editingFinished();
void on_overwriteSteamAppID_toggled(bool checked);
void on_createFilesInMod_toggled(bool checked);
void on_forceLoadLibraries_toggled(bool checked);
@@ -192,6 +199,10 @@ private:
// forced libraries set in the dialog
ForcedLibraries m_forcedLibraries;
+ // remembers the last executable title that made sense, reverts to this when
+ // the widget loses focus if it's empty
+ QString m_lastGoodTitle;
+
// true when the change events being triggered are in response to loading
// the executable's data into the UI, not from a user change
bool m_settingUI;
@@ -213,10 +224,19 @@ private:
void saveOrder();
bool canMove(const QListWidgetItem* item, int direction);
void move(QListWidgetItem* item, int direction);
- void setJarBinary(const QString& binaryName);
bool isTitleConflicting(const QString& s);
void commitChanges();
void setDirty(bool b);
+ void selectIndex(int i);
+
+ void addFromFile();
+ void addEmpty();
+ void clone();
+ void addNew(Executable e);
+
+ QFileInfo browseBinary(const QString& initial);
+ void setBinary(const QFileInfo& binary);
+ void setJarBinary(const QFileInfo& binary);
};
#endif // EDITEXECUTABLESDIALOG_H
diff --git a/src/editexecutablesdialog.ui b/src/editexecutablesdialog.ui
index a42dbeed..6f50b3b3 100644
--- a/src/editexecutablesdialog.ui
+++ b/src/editexecutablesdialog.ui
@@ -106,6 +106,9 @@
<iconset resource="resources.qrc">
<normaloff>:/MO/gui/add</normaloff>:/MO/gui/add</iconset>
</property>
+ <property name="popupMode">
+ <enum>QToolButton::InstantPopup</enum>
+ </property>
</widget>
</item>
<item>
@@ -140,7 +143,7 @@
<string>Move the executable up in the list</string>
</property>
<property name="text">
- <string>Move the executable up in the list</string>
+ <string>Up</string>
</property>
<property name="icon">
<iconset resource="resources.qrc">
@@ -160,7 +163,7 @@
<string>Move the executable down in the list</string>
</property>
<property name="text">
- <string>Move the executable down in the list</string>
+ <string>Down</string>
</property>
<property name="icon">
<iconset resource="resources.qrc">
@@ -375,7 +378,7 @@ Right now the only case I know of where this needs to be overwritten is for the
<string>If this is enabled, new files are created in the specified mod instead of the &quot;Overwrite&quot; mod.</string>
</property>
<property name="text">
- <string>Create Files in Mod instead of Overwrite (*)</string>
+ <string>Create files in mod instead of overwrite (*)</string>
</property>
</widget>
</item>
@@ -396,7 +399,7 @@ Right now the only case I know of where this needs to be overwritten is for the
<string>If this is enabled, the configured libraries will be automatically loaded when this executable is launched.</string>
</property>
<property name="text">
- <string>Force Load Libraries (*)</string>
+ <string>Force load libraries (*)</string>
</property>
</widget>
</item>
@@ -428,14 +431,27 @@ Right now the only case I know of where this needs to be overwritten is for the
<item>
<widget class="QCheckBox" name="useApplicationIcon">
<property name="text">
- <string>Use Application's Icon for shortcuts</string>
+ <string>Use application's icon for desktop shortcuts</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="hide">
+ <property name="toolTip">
+ <string>This executable will not appear in the list, on the toolbar or in the menu. It will still be visible in this dialog.</string>
+ </property>
+ <property name="whatsThis">
+ <string>This executable will not appear in the list, on the toolbar or in the menu. It will still be visible in this dialog.</string>
+ </property>
+ <property name="text">
+ <string>Hide in user interface</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_5">
<property name="text">
- <string>(*) Profile Specific</string>
+ <string>(*) Profile specific</string>
</property>
<property name="margin">
<number>5</number>
@@ -475,14 +491,25 @@ Right now the only case I know of where this needs to be overwritten is for the
</layout>
</widget>
<tabstops>
+ <tabstop>add</tabstop>
+ <tabstop>remove</tabstop>
+ <tabstop>up</tabstop>
+ <tabstop>down</tabstop>
+ <tabstop>reset</tabstop>
+ <tabstop>list</tabstop>
+ <tabstop>title</tabstop>
<tabstop>binary</tabstop>
<tabstop>browseBinary</tabstop>
<tabstop>workingDirectory</tabstop>
<tabstop>browseWorkingDirectory</tabstop>
+ <tabstop>arguments</tabstop>
<tabstop>overwriteSteamAppID</tabstop>
<tabstop>steamAppID</tabstop>
<tabstop>createFilesInMod</tabstop>
<tabstop>mods</tabstop>
+ <tabstop>forceLoadLibraries</tabstop>
+ <tabstop>configureLibraries</tabstop>
+ <tabstop>useApplicationIcon</tabstop>
</tabstops>
<resources>
<include location="resources.qrc"/>
diff --git a/src/env.cpp b/src/env.cpp
new file mode 100644
index 00000000..4c0aeb86
--- /dev/null
+++ b/src/env.cpp
@@ -0,0 +1,1112 @@
+#include "env.h"
+#include "envmetrics.h"
+#include "envmodule.h"
+#include "envsecurity.h"
+#include "envshortcut.h"
+#include "envwindows.h"
+#include "settings.h"
+#include <log.h>
+#include <utility.h>
+
+namespace env
+{
+
+using namespace MOBase;
+
+Console::Console()
+ : m_hasConsole(false), m_in(nullptr), m_out(nullptr), m_err(nullptr)
+{
+ // open a console
+ if (!AllocConsole()) {
+ // failed, ignore
+ }
+
+ m_hasConsole = true;
+
+ // redirect stdin, stdout and stderr to it
+ freopen_s(&m_in, "CONIN$", "r", stdin);
+ freopen_s(&m_out, "CONOUT$", "w", stdout);
+ freopen_s(&m_err, "CONOUT$", "w", stderr);
+}
+
+Console::~Console()
+{
+ // close redirected handles and redirect standard stream to NUL in case
+ // they're used after this
+
+ if (m_err) {
+ std::fclose(m_err);
+ freopen_s(&m_err, "NUL", "w", stderr);
+ }
+
+ if (m_out) {
+ std::fclose(m_out);
+ freopen_s(&m_out, "NUL", "w", stdout);
+ }
+
+ if (m_in) {
+ std::fclose(m_in);
+ freopen_s(&m_in, "NUL", "r", stdin);
+ }
+
+ // close console
+ if (m_hasConsole) {
+ FreeConsole();
+ }
+}
+
+
+ModuleNotification::ModuleNotification(QObject* o, std::function<void (Module)> f)
+ : m_cookie(nullptr), m_object(o), m_f(std::move(f))
+{
+}
+
+ModuleNotification::~ModuleNotification()
+{
+ if (!m_cookie) {
+ return;
+ }
+
+ typedef NTSTATUS NTAPI LdrUnregisterDllNotificationType(
+ PVOID Cookie
+ );
+
+ LibraryPtr ntdll(LoadLibraryW(L"ntdll.dll"));
+
+ if (!ntdll) {
+ log::error("failed to load ntdll.dll while unregistering for module notifications");
+ return;
+ }
+
+ auto* LdrUnregisterDllNotification = reinterpret_cast<LdrUnregisterDllNotificationType*>(
+ GetProcAddress(ntdll.get(), "LdrUnregisterDllNotification"));
+
+ if (!LdrUnregisterDllNotification) {
+ log::error("LdrUnregisterDllNotification not found in ntdll.dll");
+ return;
+ }
+
+ const auto r = LdrUnregisterDllNotification(m_cookie);
+ if (r != 0) {
+ log::error("failed to unregister for module notifications, error {}", r);
+ }
+}
+
+void ModuleNotification::setCookie(void* c)
+{
+ m_cookie = c;
+}
+
+void ModuleNotification::fire(QString path, std::size_t fileSize)
+{
+ if (m_loaded.contains(path)) {
+ // don't notify if it's been loaded before
+ return;
+ }
+
+ m_loaded.insert(path);
+
+ // constructing a Module will query the version info of the file, which seems
+ // to generate an access violation for at least plugin_python.dll on Windows 7
+ //
+ // it's not clear what the problem is, but making sure this is deferred until
+ // _after_ the dll is loaded seems to fix it
+ //
+ // so this queues the callback in the main thread
+
+ if (m_f) {
+ QMetaObject::invokeMethod(m_object, [path, fileSize, f=m_f] {
+ f(Module(path, fileSize));
+ }, Qt::QueuedConnection);
+ }
+}
+
+
+Environment::Environment()
+{
+}
+
+// anchor
+Environment::~Environment() = default;
+
+const std::vector<Module>& Environment::loadedModules() const
+{
+ if (m_modules.empty()){
+ m_modules = getLoadedModules();
+ }
+
+ return m_modules;
+}
+
+std::vector<Process> Environment::runningProcesses() const
+{
+ return getRunningProcesses();
+}
+
+const WindowsInfo& Environment::windowsInfo() const
+{
+ if (!m_windows) {
+ m_windows.reset(new WindowsInfo);
+ }
+
+ return *m_windows;
+}
+
+const std::vector<SecurityProduct>& Environment::securityProducts() const
+{
+ if (m_security.empty()) {
+ m_security = getSecurityProducts();
+ }
+
+ return m_security;
+}
+
+const Metrics& Environment::metrics() const
+{
+ if (!m_metrics) {
+ m_metrics.reset(new Metrics);
+ }
+
+ return *m_metrics;
+}
+
+QString Environment::timezone() const
+{
+ TIME_ZONE_INFORMATION tz = {};
+
+ const auto r = GetTimeZoneInformation(&tz);
+ if (r == TIME_ZONE_ID_INVALID) {
+ const auto e = GetLastError();
+ log::error("failed to get timezone, {}", formatSystemMessage(e));
+ return "unknown";
+ }
+
+ auto offsetString = [](int o) {
+ return
+ QString("%1%2:%3")
+ .arg(o < 0 ? "" : "+")
+ .arg(QString::number(o / 60), 2, QChar::fromLatin1('0'))
+ .arg(QString::number(o % 60), 2, QChar::fromLatin1('0'));
+ };
+
+ const auto stdName = QString::fromWCharArray(tz.StandardName);
+ const auto stdOffset = -(tz.Bias + tz.StandardBias);
+ const auto std = QString("%1, %2")
+ .arg(stdName)
+ .arg(offsetString(stdOffset));
+
+ const auto dstName = QString::fromWCharArray(tz.DaylightName);
+ const auto dstOffset = -(tz.Bias + tz.DaylightBias);
+ const auto dst = QString("%1, %2")
+ .arg(dstName)
+ .arg(offsetString(dstOffset));
+
+ QString s;
+
+ if (r == TIME_ZONE_ID_DAYLIGHT) {
+ s = dst + " (dst is active, std is " + std + ")";
+ } else {
+ s = std + " (std is active, dst is " + dst + ")";
+ }
+
+ return s;
+}
+
+std::unique_ptr<ModuleNotification> Environment::onModuleLoaded(
+ QObject* o, std::function<void (Module)> f)
+{
+ typedef struct _UNICODE_STRING {
+ USHORT Length;
+ USHORT MaximumLength;
+ PWSTR Buffer;
+ } UNICODE_STRING, *PUNICODE_STRING;
+
+ typedef const PUNICODE_STRING PCUNICODE_STRING;
+
+ typedef struct _LDR_DLL_LOADED_NOTIFICATION_DATA {
+ ULONG Flags; //Reserved.
+ PCUNICODE_STRING FullDllName; //The full path name of the DLL module.
+ PCUNICODE_STRING BaseDllName; //The base file name of the DLL module.
+ PVOID DllBase; //A pointer to the base address for the DLL in memory.
+ ULONG SizeOfImage; //The size of the DLL image, in bytes.
+ } LDR_DLL_LOADED_NOTIFICATION_DATA, *PLDR_DLL_LOADED_NOTIFICATION_DATA;
+
+ typedef struct _LDR_DLL_UNLOADED_NOTIFICATION_DATA {
+ ULONG Flags; //Reserved.
+ PCUNICODE_STRING FullDllName; //The full path name of the DLL module.
+ PCUNICODE_STRING BaseDllName; //The base file name of the DLL module.
+ PVOID DllBase; //A pointer to the base address for the DLL in memory.
+ ULONG SizeOfImage; //The size of the DLL image, in bytes.
+ } LDR_DLL_UNLOADED_NOTIFICATION_DATA, *PLDR_DLL_UNLOADED_NOTIFICATION_DATA;
+
+ typedef union _LDR_DLL_NOTIFICATION_DATA {
+ LDR_DLL_LOADED_NOTIFICATION_DATA Loaded;
+ LDR_DLL_UNLOADED_NOTIFICATION_DATA Unloaded;
+ } LDR_DLL_NOTIFICATION_DATA, *PLDR_DLL_NOTIFICATION_DATA;
+
+ typedef VOID CALLBACK LDR_DLL_NOTIFICATION_FUNCTION(
+ ULONG NotificationReason,
+ const PLDR_DLL_NOTIFICATION_DATA NotificationData,
+ PVOID Context
+ );
+
+ typedef LDR_DLL_NOTIFICATION_FUNCTION* PLDR_DLL_NOTIFICATION_FUNCTION;
+
+ typedef NTSTATUS NTAPI LdrRegisterDllNotificationType(
+ ULONG Flags,
+ PLDR_DLL_NOTIFICATION_FUNCTION NotificationFunction,
+ PVOID Context,
+ PVOID *Cookie
+ );
+
+ const ULONG LDR_DLL_NOTIFICATION_REASON_LOADED = 1;
+ const ULONG LDR_DLL_NOTIFICATION_REASON_UNLOADED = 2;
+
+
+ // loading ntdll.dll, the function will be found with GetProcAddress()
+ LibraryPtr ntdll(LoadLibraryW(L"ntdll.dll"));
+
+ if (!ntdll) {
+ log::error("failed to load ntdll.dll while registering for module notifications");
+ return {};
+ }
+
+ auto* LdrRegisterDllNotification = reinterpret_cast<LdrRegisterDllNotificationType*>(
+ GetProcAddress(ntdll.get(), "LdrRegisterDllNotification"));
+
+ if (!LdrRegisterDllNotification) {
+ log::error("LdrRegisterDllNotification not found in ntdll.dll");
+ return {};
+ }
+
+
+ auto context = std::make_unique<ModuleNotification>(o, f);
+ void* cookie = nullptr;
+
+ auto OnDllLoaded = [](ULONG reason, const PLDR_DLL_NOTIFICATION_DATA data, void* context) {
+ if (reason == LDR_DLL_NOTIFICATION_REASON_LOADED) {
+ if (data && data->Loaded.FullDllName) {
+ if (context) {
+ static_cast<ModuleNotification*>(context)->fire(
+ QString::fromWCharArray(
+ data->Loaded.FullDllName->Buffer,
+ data->Loaded.FullDllName->Length / sizeof(wchar_t)),
+ data->Loaded.SizeOfImage);
+ }
+ }
+ }
+ };
+
+ const auto r = LdrRegisterDllNotification(
+ 0, OnDllLoaded, context.get(), &cookie);
+
+ if (r != 0) {
+ log::error("failed to register for module notifications, error {}", r);
+ return {};
+ }
+
+ context->setCookie(cookie);
+
+ return context;
+}
+
+void Environment::dump(const Settings& s) const
+{
+ log::debug("windows: {}", windowsInfo().toString());
+
+ log::debug("time zone: {}", timezone());
+
+ if (windowsInfo().compatibilityMode()) {
+ log::warn("MO seems to be running in compatibility mode");
+ }
+
+ log::debug("security products:");
+
+ {
+ // ignore products with identical names, some AVs register themselves with
+ // the same names and provider, but different guids
+ std::set<QString> productNames;
+ for (const auto& sp : securityProducts()) {
+ productNames.insert(sp.toString());
+ }
+
+ for (auto&& name : productNames) {
+ log::debug(" . {}", name);
+ }
+ }
+
+ log::debug("modules loaded in process:");
+ for (const auto& m : loadedModules()) {
+ log::debug(" . {}", m.toString());
+ }
+
+ log::debug("displays:");
+ for (const auto& d : metrics().displays()) {
+ log::debug(" . {}", d.toString());
+ }
+
+ const auto r = metrics().desktopGeometry();
+ log::debug(
+ "desktop geometry: ({},{})-({},{})",
+ r.left(), r.top(), r.right(), r.bottom());
+
+ dumpDisks(s);
+}
+
+void Environment::dumpDisks(const Settings& s) const
+{
+ std::set<QString> rootPaths;
+
+ auto dump = [&](auto&& path) {
+ const QFileInfo fi(path);
+ const QStorageInfo si(fi.absoluteFilePath());
+
+ if (rootPaths.contains(si.rootPath())) {
+ // already seen
+ return;
+ }
+
+ // remember
+ rootPaths.insert(si.rootPath());
+
+ log::debug(
+ " . {} free={} MB{}",
+ si.rootPath(),
+ (si.bytesFree() / 1000 / 1000),
+ (si.isReadOnly() ? " (readonly)" : ""));
+ };
+
+ log::debug("drives:");
+
+ dump(QStorageInfo::root().rootPath());
+ dump(s.paths().base());
+ dump(s.paths().downloads());
+ dump(s.paths().mods());
+ dump(s.paths().cache());
+ dump(s.paths().profiles());
+ dump(s.paths().overwrite());
+ dump(QCoreApplication::applicationDirPath());
+}
+
+
+QString path()
+{
+ return get("PATH");
+}
+
+QString addPath(const QString& s)
+{
+ auto old = path();
+ set("PATH", get("PATH") + ";" + s);
+ return old;
+}
+
+QString setPath(const QString& s)
+{
+ return set("PATH", s);
+}
+
+QString get(const QString& name)
+{
+ std::size_t bufferSize = 4000;
+ auto buffer = std::make_unique<wchar_t[]>(bufferSize);
+
+ DWORD realSize = ::GetEnvironmentVariableW(
+ name.toStdWString().c_str(),
+ buffer.get(), static_cast<DWORD>(bufferSize));
+
+ if (realSize > bufferSize) {
+ bufferSize = realSize;
+ buffer = std::make_unique<wchar_t[]>(bufferSize);
+
+ realSize = ::GetEnvironmentVariableW(
+ name.toStdWString().c_str(),
+ buffer.get(), static_cast<DWORD>(bufferSize));
+ }
+
+ if (realSize == 0) {
+ const auto e = ::GetLastError();
+
+ // don't log if not found
+ if (e != ERROR_ENVVAR_NOT_FOUND) {
+ log::error(
+ "failed to get environment variable '{}', {}",
+ name, formatSystemMessage(e));
+ }
+
+ return {};
+ }
+
+ return QString::fromWCharArray(buffer.get(), realSize);
+}
+
+QString set(const QString& n, const QString& v)
+{
+ auto old = get(n);
+ ::SetEnvironmentVariableW(n.toStdWString().c_str(), v.toStdWString().c_str());
+ return old;
+}
+
+
+Service::Service(QString name)
+ : Service(std::move(name), StartType::None, Status::None)
+{
+}
+
+Service::Service(QString name, StartType st, Status s)
+ : m_name(std::move(name)), m_startType(st), m_status(s)
+{
+}
+
+const QString& Service::name() const
+{
+ return m_name;
+}
+
+bool Service::isValid() const
+{
+ return (m_startType != StartType::None) && (m_status != Status::None);
+}
+
+Service::StartType Service::startType() const
+{
+ return m_startType;
+}
+
+Service::Status Service::status() const
+{
+ return m_status;
+}
+
+QString Service::toString() const
+{
+ return QString("service '%1', start=%2, status=%3")
+ .arg(m_name)
+ .arg(env::toString(m_startType))
+ .arg(env::toString(m_status));
+}
+
+
+QString toString(Service::StartType st)
+{
+ using ST = Service::StartType;
+
+ switch (st)
+ {
+ case ST::None:
+ return "none";
+
+ case ST::Disabled:
+ return "disabled";
+
+ case ST::Enabled:
+ return "enabled";
+
+ default:
+ return QString("unknown %1").arg(static_cast<int>(st));
+ }
+}
+
+QString toString(Service::Status st)
+{
+ using S = Service::Status;
+
+ switch (st)
+ {
+ case S::None:
+ return "none";
+
+ case S::Stopped:
+ return "stopped";
+
+ case S::Running:
+ return "running";
+
+ default:
+ return QString("unknown %1").arg(static_cast<int>(st));
+ }
+}
+
+Service::StartType getServiceStartType(SC_HANDLE s, const QString& name)
+{
+ DWORD needed = 0;
+
+ if (!QueryServiceConfig(s, NULL, 0, &needed)) {
+ const auto e = GetLastError();
+
+ if (e != ERROR_INSUFFICIENT_BUFFER) {
+ log::error(
+ "QueryServiceConfig() for size for '{}' failed, {}",
+ name, GetLastError());
+
+ return Service::StartType::None;
+ }
+ }
+
+ const auto size = needed;
+ MallocPtr<QUERY_SERVICE_CONFIG> config(
+ static_cast<QUERY_SERVICE_CONFIG*>(std::malloc(size)));
+
+ if (!QueryServiceConfig(s, config.get(), size, &needed)) {
+ const auto e = GetLastError();
+
+ log::error(
+ "QueryServiceConfig() for '{}' failed", name, formatSystemMessage(e));
+
+ return Service::StartType::None;
+ }
+
+
+ switch (config->dwStartType)
+ {
+ case SERVICE_AUTO_START: // fall-through
+ case SERVICE_BOOT_START:
+ case SERVICE_DEMAND_START:
+ case SERVICE_SYSTEM_START:
+ {
+ return Service::StartType::Enabled;
+ }
+
+ case SERVICE_DISABLED:
+ {
+ return Service::StartType::Disabled;
+ }
+
+ default:
+ {
+ log::error(
+ "unknown service start type {} for '{}'",
+ config->dwStartType, name);
+
+ return Service::StartType::None;
+ }
+ }
+}
+
+Service::Status getServiceStatus(SC_HANDLE s, const QString& name)
+{
+ DWORD needed = 0;
+
+ if (!QueryServiceStatusEx(s, SC_STATUS_PROCESS_INFO, NULL, 0, &needed)) {
+ const auto e = GetLastError();
+
+ if (e != ERROR_INSUFFICIENT_BUFFER) {
+ log::error(
+ "QueryServiceStatusEx() for size for '{}' failed, {}",
+ name, GetLastError());
+
+ return Service::Status::None;
+ }
+ }
+
+ const auto size = needed;
+ MallocPtr<SERVICE_STATUS_PROCESS> status(
+ static_cast<SERVICE_STATUS_PROCESS*>(std::malloc(size)));
+
+ const auto r = QueryServiceStatusEx(
+ s, SC_STATUS_PROCESS_INFO, reinterpret_cast<BYTE*>(status.get()),
+ size, &needed);
+
+ if (!r) {
+ const auto e = GetLastError();
+
+ log::error(
+ "QueryServiceStatusEx() failed for '{}', {}",
+ name, formatSystemMessage(e));
+
+ return Service::Status::None;
+ }
+
+
+ switch (status->dwCurrentState)
+ {
+ case SERVICE_START_PENDING: // fall-through
+ case SERVICE_CONTINUE_PENDING:
+ case SERVICE_RUNNING:
+ {
+ return Service::Status::Running;
+ }
+
+ case SERVICE_STOPPED: // fall-through
+ case SERVICE_STOP_PENDING:
+ case SERVICE_PAUSE_PENDING:
+ case SERVICE_PAUSED:
+ {
+ return Service::Status::Stopped;
+ }
+
+ default:
+ {
+ log::error(
+ "unknown service status {} for '{}'",
+ status->dwCurrentState, name);
+
+ return Service::Status::None;
+ }
+ }
+}
+
+Service getService(const QString& name)
+{
+ // service manager
+ const LocalPtr<SC_HANDLE> scm(OpenSCManager(
+ NULL, NULL, SERVICE_QUERY_STATUS | SERVICE_QUERY_CONFIG));
+
+ if (!scm) {
+ const auto e = GetLastError();
+ log::error("OpenSCManager() failed, {}", formatSystemMessage(e));
+ return Service(name);
+ }
+
+ // service
+ const LocalPtr<SC_HANDLE> s(OpenService(
+ scm.get(), name.toStdWString().c_str(),
+ SERVICE_QUERY_STATUS | SERVICE_QUERY_CONFIG));
+
+ if (!s) {
+ const auto e = GetLastError();
+ log::error("OpenService() failed for '{}', {}", name, formatSystemMessage(e));
+ return Service(name);
+ }
+
+ const auto startType = getServiceStartType(s.get(), name);
+ const auto status = getServiceStatus(s.get(), name);
+
+ return {name, startType, status};
+}
+
+
+std::optional<QString> getAssocString(const QFileInfo& file, ASSOCSTR astr)
+{
+ const auto ext = L"." + file.suffix().toStdWString();
+
+ // getting buffer size
+ DWORD bufferSize = 0;
+ auto r = AssocQueryStringW(
+ ASSOCF_INIT_IGNOREUNKNOWN, astr, ext.c_str(), L"open", nullptr, &bufferSize);
+
+ // returns S_FALSE when giving back the buffer size, so that's actually the
+ // expected return value
+
+ if (r != S_FALSE || bufferSize == 0) {
+ if (r == HRESULT_FROM_WIN32(ERROR_NO_ASSOCIATION)) {
+ log::error("file '{}' has no associated executable", file.absoluteFilePath());
+ } else {
+ log::error(
+ "can't get buffer size for AssocQueryStringW(), {}",
+ formatSystemMessage(r));
+ }
+ return {};
+ }
+
+ // getting string
+ auto buffer = std::make_unique<wchar_t[]>(bufferSize + 1);
+ std::fill(buffer.get(), buffer.get() + bufferSize + 1, 0);
+
+ r = AssocQueryStringW(
+ ASSOCF_INIT_IGNOREUNKNOWN, astr, ext.c_str(), L"open", buffer.get(), &bufferSize);
+
+ if (FAILED(r)) {
+ log::error(
+ "failed to get exe associated with '{}', {}",
+ file.suffix(), formatSystemMessage(r));
+
+ return {};
+ }
+
+ // buffer size includes the null terminator
+ return QString::fromWCharArray(buffer.get(), bufferSize - 1);
+}
+
+QString formatCommandLine(const QFileInfo& targetInfo, const QString& cmd)
+{
+ // yeah, FormatMessage() expects at least as many arguments as there are
+ // placeholders and while the command for associations should typically only
+ // have %1, the user can actually enter anything in the registry
+ //
+ // since the maximum number of arguments is 99, this creates an array of 99
+ // wchar_* where the first one (%1) points to the filename and the remaining
+ // 98 to ""
+ //
+ // FormatMessage() actually takes a va_list* for the arguments, but by passing
+ // FORMAT_MESSAGE_ARGUMENT_ARRAY, an array of DWORD_PTR can be given instead
+
+ // 99 arguments
+ std::array<DWORD_PTR, 99> args;
+
+ // first one is the filename
+ const auto wpath = targetInfo.absoluteFilePath().toStdWString();
+ args[0] = reinterpret_cast<DWORD_PTR>(wpath.c_str());
+
+ // remaining are ""
+ std::fill(args.begin() + 1, args.end(), reinterpret_cast<DWORD_PTR>(L""));
+
+ // must be freed with LocalFree()
+ wchar_t* buffer = nullptr;
+
+ const auto wcmd = cmd.toStdWString();
+
+ const auto n = ::FormatMessageW(
+ FORMAT_MESSAGE_ALLOCATE_BUFFER |
+ FORMAT_MESSAGE_ARGUMENT_ARRAY |
+ FORMAT_MESSAGE_FROM_STRING,
+ wcmd.c_str(), 0, 0,
+ reinterpret_cast<LPWSTR>(&buffer),
+ 0, reinterpret_cast<va_list*>(&args[0]));
+
+ if (n == 0 || !buffer){
+ const auto e = GetLastError();
+
+ log::error(
+ "failed to format command line '{}' with path '{}', {}",
+ cmd, targetInfo.absoluteFilePath(), formatSystemMessage(e));
+
+ return {};
+ }
+
+ auto s = QString::fromWCharArray(buffer, n);
+ ::LocalFree(buffer);
+
+ return s.trimmed();
+}
+
+std::pair<QString, QString> splitExeAndArguments(const QString& cmd)
+{
+ int exeBegin = 0;
+ int exeEnd = -1;
+
+ if (cmd[0] == '"'){
+ // surrounded by double-quotes, so find the next one
+ exeBegin = 1;
+ exeEnd = cmd.indexOf('"', exeBegin);
+
+ if (exeEnd == -1) {
+ log::error("missing terminating double-quote in command line '{}'", cmd);
+ return {};
+ }
+ } else {
+ // no double-quotes, find the first whitespace
+ exeEnd = cmd.indexOf(QRegExp("\\s"));
+ if (exeEnd == -1) {
+ exeEnd = cmd.size();
+ }
+ }
+
+ QString exe = cmd.mid(exeBegin, exeEnd - exeBegin).trimmed();
+ QString args = cmd.mid(exeEnd + 1).trimmed();
+
+ return {std::move(exe), std::move(args)};
+}
+
+Association getAssociation(const QFileInfo& targetInfo)
+{
+ log::debug(
+ "getting association for '{}', extension is '.{}'",
+ targetInfo.absoluteFilePath(), targetInfo.suffix());
+
+ const auto cmd = getAssocString(targetInfo, ASSOCSTR_COMMAND);
+ if (!cmd) {
+ return {};
+ }
+
+ log::debug("raw cmd is '{}'", *cmd);
+
+ QString formattedCmd = formatCommandLine(targetInfo, *cmd);
+ if (formattedCmd.isEmpty()) {
+ log::error(
+ "command line associated with '{}' is empty",
+ targetInfo.absoluteFilePath());
+
+ return {};
+ }
+
+ log::debug("formatted cmd is '{}'", formattedCmd);
+
+ const auto p = splitExeAndArguments(formattedCmd);
+ if (p.first.isEmpty()) {
+ return {};
+ }
+
+ log::debug("split into exe='{}' and cmd='{}'", p.first, p.second);
+
+ return {p.first, *cmd, p.second};
+}
+
+
+// returns the filename of the given process or the current one
+//
+std::wstring processFilename(HANDLE process=INVALID_HANDLE_VALUE)
+{
+ // double the buffer size 10 times
+ const int MaxTries = 10;
+
+ DWORD bufferSize = MAX_PATH;
+
+ for (int tries=0; tries<MaxTries; ++tries)
+ {
+ auto buffer = std::make_unique<wchar_t[]>(bufferSize + 1);
+ std::fill(buffer.get(), buffer.get() + bufferSize + 1, 0);
+
+ DWORD writtenSize = 0;
+
+ if (process == INVALID_HANDLE_VALUE) {
+ // query this process
+ writtenSize = GetModuleFileNameW(0, buffer.get(), bufferSize);
+ } else {
+ // query another process
+ writtenSize = GetModuleBaseNameW(process, 0, buffer.get(), bufferSize);
+ }
+
+ if (writtenSize == 0) {
+ // hard failure
+ const auto e = GetLastError();
+ std::wcerr << formatSystemMessage(e) << L"\n";
+ break;
+ } else if (writtenSize >= bufferSize) {
+ // buffer is too small, try again
+ bufferSize *= 2;
+ } else {
+ // if GetModuleFileName() works, `writtenSize` does not include the null
+ // terminator
+ const std::wstring s(buffer.get(), writtenSize);
+ const std::filesystem::path path(s);
+
+ return path.filename().native();
+ }
+ }
+
+ // something failed or the path is way too long to make sense
+
+ std::wstring what;
+ if (process == INVALID_HANDLE_VALUE) {
+ what = L"the current process";
+ } else {
+ what = L"pid " + std::to_wstring(reinterpret_cast<std::uintptr_t>(process));
+ }
+
+ std::wcerr << L"failed to get filename for " << what << L"\n";
+ return {};
+}
+
+DWORD findOtherPid()
+{
+ const std::wstring defaultName = L"ModOrganizer.exe";
+
+ std::wclog << L"looking for the other process...\n";
+
+ // used to skip the current process below
+ const auto thisPid = GetCurrentProcessId();
+ std::wclog << L"this process id is " << thisPid << L"\n";
+
+ // getting the filename for this process, assumes the other process has the
+ // same one
+ auto filename = processFilename();
+ if (filename.empty()) {
+ std::wcerr
+ << L"can't get current process filename, defaulting to "
+ << defaultName << L"\n";
+
+ filename = defaultName;
+ } else {
+ std::wclog << L"this process filename is " << filename << L"\n";
+ }
+
+ // getting all running processes
+ const auto processes = getRunningProcesses();
+ std::wclog << L"there are " << processes.size() << L" processes running\n";
+
+ // going through processes, trying to find one with the same name and a
+ // different pid than this process has
+ for (const auto& p : processes) {
+ if (p.name() == filename) {
+ if (p.pid() != thisPid) {
+ return p.pid();
+ }
+ }
+ }
+
+ std::wclog
+ << L"no process with this filename\n"
+ << L"MO may not be running, or it may be running as administrator\n"
+ << L"you can try running this again as administrator\n";
+
+ return 0;
+}
+
+std::wstring tempDir()
+{
+ const DWORD bufferSize = MAX_PATH + 1;
+ wchar_t buffer[bufferSize + 1] = {};
+
+ const auto written = GetTempPathW(bufferSize, buffer);
+ if (written == 0) {
+ const auto e = GetLastError();
+
+ std::wcerr
+ << L"failed to get temp path, " << formatSystemMessage(e) << L"\n";
+
+ return {};
+ }
+
+ // `written` does not include the null terminator
+ return std::wstring(buffer, buffer + written);
+}
+
+HandlePtr tempFile(const std::wstring dir)
+{
+ // maximum tries of incrementing the counter
+ const int MaxTries = 100;
+
+ // UTC time and date will be in the filename
+ const auto now = std::time(0);
+ const auto tm = std::gmtime(&now);
+
+ // "ModOrganizer-YYYYMMDDThhmmss.dmp", with a possible "-i" appended, where
+ // i can go until MaxTries
+ std::wostringstream oss;
+ oss
+ << L"ModOrganizer-"
+ << std::setw(4) << (1900 + tm->tm_year)
+ << std::setw(2) << std::setfill(L'0') << (tm->tm_mon + 1)
+ << std::setw(2) << std::setfill(L'0') << tm->tm_mday << "T"
+ << std::setw(2) << std::setfill(L'0') << tm->tm_hour
+ << std::setw(2) << std::setfill(L'0') << tm->tm_min
+ << std::setw(2) << std::setfill(L'0') << tm->tm_sec;
+
+ const std::wstring prefix = oss.str();
+ const std::wstring ext = L".dmp";
+
+ // first path to try, without counter in it
+ std::wstring path = dir + L"\\" + prefix + ext;
+
+ for (int i=0; i<MaxTries; ++i) {
+ std::wclog << L"trying file '" << path << L"'\n";
+
+ HandlePtr h (CreateFileW(
+ path.c_str(), GENERIC_WRITE, 0, nullptr,
+ CREATE_NEW, FILE_ATTRIBUTE_NORMAL, nullptr));
+
+ if (h.get() != INVALID_HANDLE_VALUE) {
+ // worked
+ return h;
+ }
+
+ const auto e = GetLastError();
+
+ if (e != ERROR_FILE_EXISTS) {
+ // probably no write access
+ std::wcerr
+ << L"failed to create dump file, " << formatSystemMessage(e) << L"\n";
+
+ return {};
+ }
+
+ // try again with "-i"
+ path = dir + L"\\" + prefix + L"-" + std::to_wstring(i + 1) + ext;
+ }
+
+ std::wcerr << L"can't create dump file, ran out of filenames\n";
+ return {};
+}
+
+HandlePtr dumpFile()
+{
+ // try the current directory
+ HandlePtr h = tempFile(L".");
+ if (h.get() != INVALID_HANDLE_VALUE) {
+ return h;
+ }
+
+ std::wclog << L"cannot write dump file in current directory\n";
+
+ // try the temp directory
+ const auto dir = tempDir();
+
+ if (!dir.empty()) {
+ h = tempFile(dir.c_str());
+ if (h.get() != INVALID_HANDLE_VALUE) {
+ return h;
+ }
+ }
+
+ return {};
+}
+
+bool createMiniDump(HANDLE process, CoreDumpTypes type)
+{
+ const DWORD pid = GetProcessId(process);
+
+ const HandlePtr file = dumpFile();
+ if (!file) {
+ std::wcerr << L"nowhere to write the dump file\n";
+ return false;
+ }
+
+ auto flags = _MINIDUMP_TYPE(
+ MiniDumpNormal |
+ MiniDumpWithHandleData |
+ MiniDumpWithUnloadedModules |
+ MiniDumpWithProcessThreadData);
+
+ if (type == CoreDumpTypes::Data) {
+ std::wclog << L"writing minidump with data\n";
+ flags = _MINIDUMP_TYPE(flags | MiniDumpWithDataSegs);
+ } else if (type == CoreDumpTypes::Full) {
+ std::wclog << L"writing full minidump\n";
+ flags = _MINIDUMP_TYPE(flags | MiniDumpWithFullMemory);
+ } else {
+ std::wclog << L"writing mini minidump\n";
+ }
+
+ const auto ret = MiniDumpWriteDump(
+ process, pid, file.get(), flags, nullptr, nullptr, nullptr);
+
+ if (!ret) {
+ const auto e = GetLastError();
+
+ std::wcerr
+ << L"failed to write mini dump, " << formatSystemMessage(e) << L"\n";
+
+ return false;
+ }
+
+ std::wclog << L"minidump written correctly\n";
+ return true;
+}
+
+
+bool coredump(CoreDumpTypes type)
+{
+ std::wclog << L"creating minidump for the current process\n";
+ return createMiniDump(GetCurrentProcess(), type);
+}
+
+bool coredumpOther(CoreDumpTypes type)
+{
+ std::wclog << L"creating minidump for an running process\n";
+
+ const auto pid = findOtherPid();
+ if (pid == 0) {
+ std::wcerr << L"no other process found\n";
+ return false;
+ }
+
+ std::wclog << L"found other process with pid " << pid << L"\n";
+
+ HandlePtr handle(OpenProcess(
+ PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, pid));
+
+ if (!handle) {
+ const auto e = GetLastError();
+
+ std::wcerr
+ << L"failed to open process " << pid << L", "
+ << formatSystemMessage(e) << L"\n";
+
+ return false;
+ }
+
+ return createMiniDump(handle.get(), type);
+}
+
+} // namespace
diff --git a/src/env.h b/src/env.h
new file mode 100644
index 00000000..16f8039e
--- /dev/null
+++ b/src/env.h
@@ -0,0 +1,287 @@
+#ifndef ENV_ENV_H
+#define ENV_ENV_H
+
+class Settings;
+
+namespace env
+{
+
+class Module;
+class Process;
+class SecurityProduct;
+class WindowsInfo;
+class Metrics;
+
+
+// used by DesktopDCPtr, calls ReleaseDC(0, dc) as the deleter
+//
+struct DesktopDCReleaser
+{
+ using pointer = HDC;
+
+ void operator()(HDC dc)
+ {
+ if (dc != 0) {
+ ::ReleaseDC(0, dc);
+ }
+ }
+};
+
+using DesktopDCPtr = std::unique_ptr<HDC, DesktopDCReleaser>;
+
+
+// used by LibraryPtr, calls FreeLibrary as the deleter
+//
+struct LibraryFreer
+{
+ using pointer = HINSTANCE;
+
+ void operator()(HINSTANCE h)
+ {
+ if (h != 0) {
+ ::FreeLibrary(h);
+ }
+ }
+};
+
+using LibraryPtr = std::unique_ptr<HINSTANCE, LibraryFreer>;
+
+
+// used by COMPtr, calls Release() as the deleter
+//
+struct COMReleaser
+{
+ void operator()(IUnknown* p)
+ {
+ if (p) {
+ p->Release();
+ }
+ }
+};
+
+template <class T>
+using COMPtr = std::unique_ptr<T, COMReleaser>;
+
+
+// used by MallocPtr, calls std::free() as the deleter
+//
+struct MallocFreer
+{
+ void operator()(void* p)
+ {
+ std::free(p);
+ }
+};
+
+template <class T>
+using MallocPtr = std::unique_ptr<T, MallocFreer>;
+
+
+// used by LocalPtr, calls LocalFree() as the deleter
+//
+template <class T>
+struct LocalFreer
+{
+ using pointer = T;
+
+ void operator()(T p)
+ {
+ ::LocalFree(p);
+ }
+};
+
+template <class T>
+using LocalPtr = std::unique_ptr<T, LocalFreer<T>>;
+
+
+// creates a console in the constructor and destroys it in the destructor,
+// also redirects standard streams
+//
+class Console
+{
+public:
+ // opens the console and redirects standard streams to it
+ //
+ Console();
+
+ // destroys the console and redirects the standard stream to NUL
+ //
+ ~Console();
+
+private:
+ // whether the console was allocated successfully
+ bool m_hasConsole;
+
+ // standard streams
+ FILE* m_in;
+ FILE* m_out;
+ FILE* m_err;
+};
+
+
+class ModuleNotification
+{
+public:
+ ModuleNotification(QObject* o, std::function<void (Module)> f);
+ ~ModuleNotification();
+
+ ModuleNotification(const ModuleNotification&) = delete;
+ ModuleNotification& operator=(const ModuleNotification&) = delete;
+
+ ModuleNotification(ModuleNotification&&) = default;
+ ModuleNotification& operator=(ModuleNotification&&) = default;
+
+ void setCookie(void* c);
+ void fire(QString path, std::size_t fileSize);
+
+private:
+ void* m_cookie;
+ QObject* m_object;
+ std::set<QString> m_loaded;
+ std::function<void (Module)> m_f;
+};
+
+
+// represents the process's environment
+//
+class Environment
+{
+public:
+ Environment();
+ ~Environment();
+
+ // list of loaded modules in the current process
+ //
+ const std::vector<Module>& loadedModules() const;
+
+ // list of running processes; not cached
+ //
+ std::vector<Process> runningProcesses() const;
+
+ // information about the operating system
+ //
+ const WindowsInfo& windowsInfo() const;
+
+ // information about the installed security products
+ //
+ const std::vector<SecurityProduct>& securityProducts() const;
+
+ // information about displays
+ //
+ const Metrics& metrics() const;
+
+ // timezone
+ //
+ QString timezone() const;
+
+ // will call `f` on the same thread `o` is running on every time a module
+ // is loaded in the process
+ //
+ std::unique_ptr<ModuleNotification> onModuleLoaded(
+ QObject* o, std::function<void (Module)> f);
+
+ // logs the environment
+ //
+ void dump(const Settings& s) const;
+
+private:
+ mutable std::vector<Module> m_modules;
+ mutable std::unique_ptr<WindowsInfo> m_windows;
+ mutable std::vector<SecurityProduct> m_security;
+ mutable std::unique_ptr<Metrics> m_metrics;
+
+ // dumps all the disks involved in the settings
+ //
+ void dumpDisks(const Settings& s) const;
+};
+
+
+// environment variables
+//
+QString get(const QString& name);
+QString set(const QString& name, const QString& value);
+
+QString path();
+QString addPath(const QString& s);
+QString setPath(const QString& s);
+
+
+class Service
+{
+public:
+ enum class StartType
+ {
+ None = 0,
+ Disabled,
+ Enabled
+ };
+
+ enum class Status
+ {
+ None = 0,
+ Stopped,
+ Running
+ };
+
+
+ explicit Service(QString name);
+ Service(QString name, StartType st, Status s);
+
+ bool isValid() const;
+
+ const QString& name() const;
+ StartType startType() const;
+ Status status() const;
+
+ QString toString() const;
+
+private:
+ QString m_name;
+ StartType m_startType;
+ Status m_status;
+};
+
+
+Service getService(const QString& name);
+QString toString(Service::StartType st);
+QString toString(Service::Status st);
+
+
+struct Association
+{
+ // path to the executable associated with the file
+ QFileInfo executable;
+
+ // full command line associated with the file, no replacements
+ QString commandLine;
+
+ // command line _without_ the executable and with placeholders such as %1
+ // replaced by the given file
+ QString formattedCommandLine;
+};
+
+// returns the associated executable and command line, executable is empty on
+// error
+//
+Association getAssociation(const QFileInfo& file);
+
+
+enum class CoreDumpTypes
+{
+ Mini = 1,
+ Data,
+ Full
+};
+
+// creates a minidump file for this process
+//
+bool coredump(CoreDumpTypes type);
+
+// finds another process with the same name as this one and creates a minidump
+// file for it
+//
+bool coredumpOther(CoreDumpTypes type);
+
+} // namespace env
+
+#endif // ENV_ENV_H
diff --git a/src/envmetrics.cpp b/src/envmetrics.cpp
new file mode 100644
index 00000000..5fb80449
--- /dev/null
+++ b/src/envmetrics.cpp
@@ -0,0 +1,266 @@
+#include "envmetrics.h"
+#include "env.h"
+#include <Windows.h>
+#include <shellscalingapi.h>
+#include <log.h>
+#include <utility.h>
+#include <QDesktopWidget>
+
+namespace env
+{
+
+using namespace MOBase;
+
+// fallback for windows 7
+//
+int getDesktopDpi()
+{
+ // desktop DC
+ DesktopDCPtr dc(GetDC(0));
+
+ if (!dc) {
+ const auto e = GetLastError();
+ log::error("can't get desktop DC, {}", formatSystemMessage(e));
+ return 0;
+ }
+
+ return GetDeviceCaps(dc.get(), LOGPIXELSX);
+}
+
+// finds a monitor by device name; there's no real good way to do that except
+// by enumerating all the monitors and checking their name
+//
+HMONITOR findMonitor(const QString& name)
+{
+ // passed to the enumeration callback
+ struct Data
+ {
+ QString name;
+ HMONITOR hm;
+ };
+
+ Data data = {name, 0};
+
+ // callback
+ auto callback = [](HMONITOR hm, HDC, RECT*, LPARAM lp) {
+ auto& data = *reinterpret_cast<Data*>(lp);
+
+ MONITORINFOEX mi = {};
+ mi.cbSize = sizeof(mi);
+
+ // monitor info will include the name
+ if (!GetMonitorInfoW(hm, &mi)) {
+ const auto e = GetLastError();
+ log::error(
+ "GetMonitorInfo() failed for '{}', {}",
+ data.name, formatSystemMessage(e));
+
+ // error for this monitor, but continue
+ return TRUE;
+ }
+
+ if (QString::fromWCharArray(mi.szDevice) == data.name) {
+ // found, stop
+ data.hm = hm;
+ return FALSE;
+ }
+
+ // not found, continue to the next monitor
+ return TRUE;
+ };
+
+
+ // for each monitor
+ EnumDisplayMonitors(0, nullptr, callback, reinterpret_cast<LPARAM>(&data));
+
+ return data.hm;
+}
+
+// returns the dpi for the given monitor; for systems that do not support
+// per-monitor dpi (such as windows 7), this is the desktop dpi
+//
+int getDpi(const QString& monitorDevice)
+{
+ using GetDpiForMonitorFunction =
+ HRESULT WINAPI (HMONITOR, MONITOR_DPI_TYPE, UINT*, UINT*);
+
+ static LibraryPtr shcore;
+ static GetDpiForMonitorFunction* GetDpiForMonitor = nullptr;
+ static bool checked = false;
+
+ if (!checked) {
+ // try to find GetDpiForMonitor() from shcored.dll
+
+ shcore.reset(LoadLibraryW(L"Shcore.dll"));
+
+ if (shcore) {
+ // windows 8.1+ only
+ GetDpiForMonitor = reinterpret_cast<GetDpiForMonitorFunction*>(
+ GetProcAddress(shcore.get(), "GetDpiForMonitor"));
+ }
+
+ checked = true;
+ }
+
+ if (!GetDpiForMonitor) {
+ // get the desktop dpi instead
+ return getDesktopDpi();
+ }
+
+
+ // there's no way to get an HMONITOR from a device name, so all monitors
+ // will have to be enumerated and their name checked
+ HMONITOR hm = findMonitor(monitorDevice);
+ if (!hm) {
+ log::error("can't get dpi for monitor '{}', not found", monitorDevice);
+ return 0;
+ }
+
+ UINT dpiX=0, dpiY=0;
+ const auto r = GetDpiForMonitor(hm, MDT_EFFECTIVE_DPI, &dpiX, &dpiY);
+
+ if (FAILED(r)) {
+ log::error(
+ "GetDpiForMonitor() failed for '{}', {}",
+ monitorDevice, formatSystemMessage(r));
+
+ return 0;
+ }
+
+ // dpiX and dpiY are always identical, as per the documentation
+ return dpiX;
+}
+
+
+Display::Display(QString adapter, QString monitorDevice, bool primary) :
+ m_adapter(std::move(adapter)),
+ m_monitorDevice(std::move(monitorDevice)),
+ m_primary(primary),
+ m_resX(0), m_resY(0), m_dpi(0), m_refreshRate(0)
+{
+ getSettings();
+ m_dpi = getDpi(m_monitorDevice);
+}
+
+const QString& Display::adapter() const
+{
+ return m_adapter;
+}
+
+const QString& Display::monitorDevice() const
+{
+ return m_monitorDevice;
+}
+
+bool Display::primary()
+{
+ return m_primary;
+}
+
+int Display::resX() const
+{
+ return m_resX;
+}
+
+int Display::resY() const
+{
+ return m_resY;
+}
+
+int Display::dpi()
+{
+ return m_dpi;
+}
+
+int Display::refreshRate() const
+{
+ return m_refreshRate;
+}
+
+QString Display::toString() const
+{
+ return QString("%1*%2 %3hz dpi=%4 on %5%6")
+ .arg(m_resX)
+ .arg(m_resY)
+ .arg(m_refreshRate)
+ .arg(m_dpi)
+ .arg(m_adapter)
+ .arg(m_primary ? " (primary)" : "");
+}
+
+void Display::getSettings()
+{
+ DEVMODEW dm = {};
+ dm.dmSize = sizeof(dm);
+
+ const auto wsDevice = m_monitorDevice.toStdWString();
+
+ if (!EnumDisplaySettingsW(wsDevice.c_str(), ENUM_CURRENT_SETTINGS, &dm)) {
+ log::error("EnumDisplaySettings() failed for '{}'", m_monitorDevice);
+ return;
+ }
+
+ // all these fields should be available
+
+ if (dm.dmFields & DM_DISPLAYFREQUENCY) {
+ m_refreshRate = dm.dmDisplayFrequency;
+ }
+
+ if (dm.dmFields & DM_PELSWIDTH) {
+ m_resX = dm.dmPelsWidth;
+ }
+
+ if (dm.dmFields & DM_PELSHEIGHT) {
+ m_resY = dm.dmPelsHeight;
+ }
+}
+
+
+Metrics::Metrics()
+{
+ getDisplays();
+}
+
+const std::vector<Display>& Metrics::displays() const
+{
+ return m_displays;
+}
+
+QRect Metrics::desktopGeometry() const
+{
+ QRect r;
+
+ for (auto* s : QGuiApplication::screens()) {
+ r = r.united(s->geometry());
+ }
+
+ return r;
+}
+
+void Metrics::getDisplays()
+{
+ // don't bother if it goes over 100
+ for (int i=0; i<100; ++i) {
+ DISPLAY_DEVICEW device = {};
+ device.cb = sizeof(device);
+
+ if (!EnumDisplayDevicesW(nullptr, i, &device, 0)) {
+ // no more
+ break;
+ }
+
+ // EnumDisplayDevices() seems to be returning a lot of devices that are
+ // not actually monitors, but those don't have the
+ // DISPLAY_DEVICE_ATTACHED_TO_DESKTOP bit set
+ if ((device.StateFlags & DISPLAY_DEVICE_ATTACHED_TO_DESKTOP) == 0) {
+ continue;
+ }
+
+ m_displays.emplace_back(
+ QString::fromWCharArray(device.DeviceString),
+ QString::fromWCharArray(device.DeviceName),
+ (device.StateFlags & DISPLAY_DEVICE_PRIMARY_DEVICE));
+ }
+}
+
+} // namespace
diff --git a/src/envmetrics.h b/src/envmetrics.h
new file mode 100644
index 00000000..8dfdb087
--- /dev/null
+++ b/src/envmetrics.h
@@ -0,0 +1,81 @@
+#ifndef ENV_METRICS_H
+#define ENV_METRICS_H
+
+#include <QString>
+#include <vector>
+
+namespace env
+{
+
+// information about a monitor
+//
+class Display
+{
+public:
+ Display(QString adapter, QString monitorDevice, bool primary);
+
+ // display name of the adapter running the monitor
+ //
+ const QString& adapter() const;
+
+ // internal device name of the monitor, this is not a display name
+ //
+ const QString& monitorDevice() const;
+
+ // whether this monitor is the primary
+ //
+ bool primary();
+
+ // resolution
+ //
+ int resX() const;
+ int resY() const;
+
+ // dpi
+ //
+ int dpi();
+
+ // refresh rate in hz
+ //
+ int refreshRate() const;
+
+ // string representation
+ //
+ QString toString() const;
+
+private:
+ QString m_adapter;
+ QString m_monitorDevice;
+ bool m_primary;
+ int m_resX, m_resY;
+ int m_dpi;
+ int m_refreshRate;
+
+ void getSettings();
+};
+
+
+// holds various information about Windows metrics
+//
+class Metrics
+{
+public:
+ Metrics();
+
+ // list of displays on the system
+ //
+ const std::vector<Display>& displays() const;
+
+ // full resolution
+ //
+ QRect desktopGeometry() const;
+
+private:
+ std::vector<Display> m_displays;
+
+ void getDisplays();
+};
+
+} // namespace
+
+#endif // ENV_METRICS_H
diff --git a/src/envmodule.cpp b/src/envmodule.cpp
new file mode 100644
index 00000000..5be52de6
--- /dev/null
+++ b/src/envmodule.cpp
@@ -0,0 +1,784 @@
+#include "envmodule.h"
+#include "env.h"
+#include <utility.h>
+#include <log.h>
+
+namespace env
+{
+
+using namespace MOBase;
+
+Module::Module(QString path, std::size_t fileSize)
+ : m_path(std::move(path)), m_fileSize(fileSize)
+{
+ const auto fi = getFileInfo();
+
+ m_version = getVersion(fi.ffi);
+ m_timestamp = getTimestamp(fi.ffi);
+ m_versionString = fi.fileDescription;
+ m_md5 = getMD5();
+}
+
+const QString& Module::path() const
+{
+ return m_path;
+}
+
+QString Module::displayPath() const
+{
+ return QDir::fromNativeSeparators(m_path.toLower());
+}
+
+std::size_t Module::fileSize() const
+{
+ return m_fileSize;
+}
+
+const QString& Module::version() const
+{
+ return m_version;
+}
+
+const QString& Module::versionString() const
+{
+ return m_versionString;
+}
+
+const QDateTime& Module::timestamp() const
+{
+ return m_timestamp;
+}
+
+const QString& Module::md5() const
+{
+ return m_md5;
+}
+
+QString Module::timestampString() const
+{
+ if (!m_timestamp.isValid()) {
+ return "(no timestamp)";
+ }
+
+ return m_timestamp.toString(Qt::DateFormat::ISODate);
+}
+
+QString Module::toString() const
+{
+ QStringList sl;
+
+ // file size
+ sl.push_back(displayPath());
+ sl.push_back(QString("%1 B").arg(m_fileSize));
+
+ // version
+ if (m_version.isEmpty() && m_versionString.isEmpty()) {
+ sl.push_back("(no version)");
+ } else {
+ if (!m_version.isEmpty()) {
+ sl.push_back(m_version);
+ }
+
+ if (!m_versionString.isEmpty() && m_versionString != m_version) {
+ sl.push_back(versionString());
+ }
+ }
+
+ // timestamp
+ if (m_timestamp.isValid()) {
+ sl.push_back(m_timestamp.toString(Qt::DateFormat::ISODate));
+ } else {
+ sl.push_back("(no timestamp)");
+ }
+
+ // md5
+ if (!m_md5.isEmpty()) {
+ sl.push_back(m_md5);
+ }
+
+ return sl.join(", ");
+}
+
+Module::FileInfo Module::getFileInfo() const
+{
+ const auto wspath = m_path.toStdWString();
+
+ // getting version info size
+ DWORD dummy = 0;
+ const DWORD size = GetFileVersionInfoSizeW(wspath.c_str(), &dummy);
+
+ if (size == 0) {
+ const auto e = GetLastError();
+
+ if (e == ERROR_RESOURCE_TYPE_NOT_FOUND) {
+ // not an error, no version information built into that module
+ return {};
+ }
+
+ log::error(
+ "GetFileVersionInfoSizeW() failed on '{}', {}",
+ m_path, formatSystemMessage(e));
+
+ return {};
+ }
+
+ // getting version info
+ auto buffer = std::make_unique<std::byte[]>(size);
+
+ if (!GetFileVersionInfoW(wspath.c_str(), 0, size, buffer.get())) {
+ const auto e = GetLastError();
+
+ log::error(
+ "GetFileVersionInfoW() failed on '{}', {}",
+ m_path, formatSystemMessage(e));
+
+ return {};
+ }
+
+ // the version info has two major parts: a fixed version and a localizable
+ // set of strings
+
+ FileInfo fi;
+ fi.ffi = getFixedFileInfo(buffer.get());
+ fi.fileDescription = getFileDescription(buffer.get());
+
+ return fi;
+}
+
+VS_FIXEDFILEINFO Module::getFixedFileInfo(std::byte* buffer) const
+{
+ void* valuePointer = nullptr;
+ unsigned int valueSize = 0;
+
+ // the fixed version info is in the root
+ const auto ret = VerQueryValueW(buffer, L"\\", &valuePointer, &valueSize);
+
+ if (!ret || !valuePointer || valueSize == 0) {
+ // not an error, no fixed file info
+ return {};
+ }
+
+ const auto* fi = static_cast<VS_FIXEDFILEINFO*>(valuePointer);
+
+ // signature is always 0xfeef04bd
+ if (fi->dwSignature != 0xfeef04bd) {
+ log::error(
+ "bad file info signature {:#x} for '{}'",
+ fi->dwSignature, m_path);
+
+ return {};
+ }
+
+ return *fi;
+}
+
+QString Module::getFileDescription(std::byte* buffer) const
+{
+ struct LANGANDCODEPAGE
+ {
+ WORD wLanguage;
+ WORD wCodePage;
+ };
+
+ void* valuePointer = nullptr;
+ unsigned int valueSize = 0;
+
+ // getting list of available languages
+ auto ret = VerQueryValueW(
+ buffer, L"\\VarFileInfo\\Translation", &valuePointer, &valueSize);
+
+ if (!ret || !valuePointer || valueSize == 0) {
+ log::error("VerQueryValueW() for translations failed on '{}'", m_path);
+ return {};
+ }
+
+ // number of languages
+ const auto count = valueSize / sizeof(LANGANDCODEPAGE);
+ if (count == 0) {
+ return {};
+ }
+
+ // using the first language in the list to get FileVersion
+ const auto* lcp = static_cast<LANGANDCODEPAGE*>(valuePointer);
+
+ const auto subBlock = QString("\\StringFileInfo\\%1%2\\FileVersion")
+ .arg(lcp->wLanguage, 4, 16, QChar('0'))
+ .arg(lcp->wCodePage, 4, 16, QChar('0'));
+
+ ret = VerQueryValueW(
+ buffer, subBlock.toStdWString().c_str(), &valuePointer, &valueSize);
+
+ if (!ret || !valuePointer || valueSize == 0) {
+ // not an error, no file version
+ return {};
+ }
+
+ // valueSize includes the null terminator
+ return QString::fromWCharArray(
+ static_cast<wchar_t*>(valuePointer), valueSize - 1);
+}
+
+QString Module::getVersion(const VS_FIXEDFILEINFO& fi) const
+{
+ if (fi.dwSignature == 0) {
+ return {};
+ }
+
+ const DWORD major = (fi.dwFileVersionMS >> 16 ) & 0xffff;
+ const DWORD minor = (fi.dwFileVersionMS >> 0 ) & 0xffff;
+ const DWORD maintenance = (fi.dwFileVersionLS >> 16 ) & 0xffff;
+ const DWORD build = (fi.dwFileVersionLS >> 0 ) & 0xffff;
+
+ if (major == 0 && minor == 0 && maintenance == 0 && build == 0) {
+ return {};
+ }
+
+ return QString("%1.%2.%3.%4")
+ .arg(major).arg(minor).arg(maintenance).arg(build);
+}
+
+QDateTime Module::getTimestamp(const VS_FIXEDFILEINFO& fi) const
+{
+ FILETIME ft = {};
+
+ if (fi.dwSignature == 0 || (fi.dwFileDateMS == 0 && fi.dwFileDateLS == 0)) {
+ // if the file info is invalid or doesn't have a date, use the creation
+ // time on the file
+
+ // opening the file
+ HandlePtr h(CreateFileW(
+ m_path.toStdWString().c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr,
+ OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0));
+
+ if (h.get() == INVALID_HANDLE_VALUE) {
+ const auto e = GetLastError();
+
+ log::error(
+ "can't open file '{}' for timestamp, {}",
+ m_path, formatSystemMessage(e));
+
+ return {};
+ }
+
+ // getting the file time
+ if (!GetFileTime(h.get(), &ft, nullptr, nullptr)) {
+ const auto e = GetLastError();
+
+ log::error(
+ "can't get file time for '{}', {}",
+ m_path, formatSystemMessage(e));
+
+ return {};
+ }
+ } else {
+ // use the time from the file info
+ ft.dwHighDateTime = fi.dwFileDateMS;
+ ft.dwLowDateTime = fi.dwFileDateLS;
+ }
+
+
+ // converting to SYSTEMTIME
+ SYSTEMTIME utc = {};
+
+ if (!FileTimeToSystemTime(&ft, &utc)) {
+ log::error(
+ "FileTimeToSystemTime() failed on timestamp high={:#x} low={:#x} for '{}'",
+ ft.dwHighDateTime, ft.dwLowDateTime, m_path);
+
+ return {};
+ }
+
+ return QDateTime(
+ QDate(utc.wYear, utc.wMonth, utc.wDay),
+ QTime(utc.wHour, utc.wMinute, utc.wSecond, utc.wMilliseconds));
+}
+
+QString Module::getMD5() const
+{
+ static const std::set<QString> ignore = {
+ "\\windows\\",
+ "\\program files\\",
+ "\\program files (x86)\\",
+ "\\programdata\\"
+ };
+
+ // don't calculate md5 for system files, it's not really relevant and
+ // it takes a while
+ for (auto&& i : ignore) {
+ if (m_path.contains(i, Qt::CaseInsensitive)) {
+ return {};
+ }
+ }
+
+ // opening the file
+ QFile f(m_path);
+
+ if (!f.open(QFile::ReadOnly)) {
+ log::error("failed to open file '{}' for md5", m_path);
+ return {};
+ }
+
+ // hashing
+ QCryptographicHash hash(QCryptographicHash::Md5);
+ if (!hash.addData(&f)) {
+ log::error("failed to calculate md5 for '{}'", m_path);
+ return {};
+ }
+
+ return hash.result().toHex();
+}
+
+
+Process::Process()
+ : Process(0, 0, {})
+{
+}
+
+Process::Process(HANDLE h)
+ : Process(::GetProcessId(h), 0, {})
+{
+}
+
+Process::Process(DWORD pid, DWORD ppid, QString name)
+ : m_pid(pid), m_ppid(ppid), m_name(std::move(name))
+{
+}
+
+bool Process::isValid() const
+{
+ return (m_pid != 0);
+}
+
+DWORD Process::pid() const
+{
+ return m_pid;
+}
+
+DWORD Process::ppid() const
+{
+ if (!m_ppid) {
+ m_ppid = getProcessParentID(m_pid);
+ }
+
+ return *m_ppid;
+}
+
+const QString& Process::name() const
+{
+ if (!m_name) {
+ m_name = getProcessName(m_pid);
+ }
+
+ return *m_name;
+}
+
+HandlePtr Process::openHandleForWait() const
+{
+ const auto rights =
+ PROCESS_QUERY_LIMITED_INFORMATION | // exit code, image name, etc.
+ SYNCHRONIZE | // wait functions
+ PROCESS_SET_QUOTA | PROCESS_TERMINATE; // add to job
+
+ // don't log errors, failure can happen if the process doesn't exist
+ return HandlePtr(OpenProcess(rights, FALSE, m_pid));
+}
+
+// whether this process can be accessed; fails if the current process doesn't
+// have the proper permissions
+//
+bool Process::canAccess() const
+{
+ HandlePtr h(OpenProcess(
+ PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, m_pid));
+
+ if (!h) {
+ const auto e = GetLastError();
+ if (e == ERROR_ACCESS_DENIED) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+void Process::addChild(Process p)
+{
+ m_children.push_back(p);
+}
+
+std::vector<Process>& Process::children()
+{
+ return m_children;
+}
+
+const std::vector<Process>& Process::children() const
+{
+ return m_children;
+}
+
+
+std::vector<Module> getLoadedModules()
+{
+ HandlePtr snapshot(CreateToolhelp32Snapshot(
+ TH32CS_SNAPMODULE32 | TH32CS_SNAPMODULE, GetCurrentProcessId()));
+
+ if (snapshot.get() == INVALID_HANDLE_VALUE)
+ {
+ const auto e = GetLastError();
+ log::error("CreateToolhelp32Snapshot() failed, {}", formatSystemMessage(e));
+ return {};
+ }
+
+ MODULEENTRY32 me = {};
+ me.dwSize = sizeof(me);
+
+ // first module, this shouldn't fail because there's at least the executable
+ if (!Module32First(snapshot.get(), &me))
+ {
+ const auto e = GetLastError();
+ log::error("Module32First() failed, {}", formatSystemMessage(e));
+ return {};
+ }
+
+ std::vector<Module> v;
+
+ for (;;)
+ {
+ const auto path = QString::fromWCharArray(me.szExePath);
+ if (!path.isEmpty()) {
+ v.push_back(Module(path, me.modBaseSize));
+ }
+
+ // next module
+ if (!Module32Next(snapshot.get(), &me)) {
+ const auto e = GetLastError();
+
+ // no more modules is not an error
+ if (e != ERROR_NO_MORE_FILES) {
+ log::error("Module32Next() failed, {}", formatSystemMessage(e));
+ }
+
+ break;
+ }
+ }
+
+ // sorting by display name
+ std::sort(v.begin(), v.end(), [](auto&& a, auto&& b) {
+ return (a.displayPath().compare(b.displayPath(), Qt::CaseInsensitive) < 0);
+ });
+
+ return v;
+}
+
+
+template <class F>
+void forEachRunningProcess(F&& f)
+{
+ HandlePtr snapshot(CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0));
+
+ if (snapshot.get() == INVALID_HANDLE_VALUE)
+ {
+ const auto e = GetLastError();
+ log::error("CreateToolhelp32Snapshot() failed, {}", formatSystemMessage(e));
+ return;
+ }
+
+ PROCESSENTRY32 entry = {};
+ entry.dwSize = sizeof(entry);
+
+ // first process, this shouldn't fail because there's at least one process
+ // running
+ if (!Process32First(snapshot.get(), &entry)) {
+ const auto e = GetLastError();
+ log::error("Process32First() failed, {}", formatSystemMessage(e));
+ return;
+ }
+
+ for (;;)
+ {
+ if (!f(entry)) {
+ break;
+ }
+
+ // next process
+ if (!Process32Next(snapshot.get(), &entry))
+ {
+ const auto e = GetLastError();
+
+ // no more processes is not an error
+ if (e != ERROR_NO_MORE_FILES)
+ log::error("Process32Next() failed, {}", formatSystemMessage(e));
+
+ break;
+ }
+ }
+}
+
+std::vector<Process> getRunningProcesses()
+{
+ std::vector<Process> v;
+
+ forEachRunningProcess([&](auto&& entry) {
+ v.push_back(Process(
+ entry.th32ProcessID,
+ entry.th32ParentProcessID,
+ QString::fromStdWString(entry.szExeFile)));
+
+ return true;
+ });
+
+ return v;
+}
+
+void findChildren(Process& parent, const std::vector<Process>& processes)
+{
+ for (auto&& p : processes) {
+ if (p.ppid() == parent.pid()) {
+ Process child = p;
+ findChildren(child, processes);
+
+ parent.addChild(child);
+ }
+ }
+}
+
+
+Process getProcessTreeFromProcess(HANDLE h)
+{
+ Process root;
+
+ const auto parentPID = ::GetProcessId(h);
+ const auto v = getRunningProcesses();
+
+ for (auto&& p : v) {
+ if (p.pid() == parentPID) {
+ Process child = p;
+ findChildren(child, v);
+ root.addChild(child);
+ break;
+ }
+ }
+
+ return root;
+}
+
+
+std::vector<DWORD> processesInJob(HANDLE h)
+{
+ for (int tries=0; tries<5; ++tries) {
+ DWORD maxIds = 100;
+
+ const DWORD idsSize = sizeof(ULONG_PTR) * maxIds;
+ const DWORD bufferSize = sizeof(JOBOBJECT_BASIC_PROCESS_ID_LIST) + idsSize;
+
+ MallocPtr<void> buffer(std::malloc(bufferSize));
+ auto* ids = static_cast<JOBOBJECT_BASIC_PROCESS_ID_LIST*>(buffer.get());
+
+ const auto r = QueryInformationJobObject(
+ h, JobObjectBasicProcessIdList, ids, bufferSize, nullptr);
+
+ if (!r) {
+ const auto e = GetLastError();
+ log::error("failed to get process ids in job, {}", formatSystemMessage(e));
+ return {};
+ }
+
+ if (ids->NumberOfProcessIdsInList >= ids->NumberOfAssignedProcesses) {
+ std::vector<DWORD> v;
+ for (DWORD i=0; i<ids->NumberOfProcessIdsInList; ++i) {
+ v.push_back(ids->ProcessIdList[i]);
+ }
+
+ return v;
+ }
+
+ // try again with a larger buffer
+ maxIds *= 2;
+ }
+
+ log::error("failed to get processes in job, can't get a buffer large enough");
+ return {};
+}
+
+
+void findChildProcesses(Process& parent, std::vector<Process>& processes)
+{
+ // find all processes that are direct children of `parent`
+ auto itor = processes.begin();
+
+ while (itor != processes.end()) {
+ if (itor->ppid() == parent.pid()) {
+ parent.addChild(*itor);
+ itor = processes.erase(itor);
+ } else {
+ ++itor;
+ }
+ }
+
+ // find all processes that are direct children of `parent`'s children
+ for (auto&& c : parent.children()) {
+ findChildProcesses(c, processes);
+ }
+}
+
+Process getProcessTreeFromJob(HANDLE h)
+{
+ const auto ids = processesInJob(h);
+ if (ids.empty()) {
+ return {};
+ }
+
+ std::vector<Process> ps;
+
+ forEachRunningProcess([&](auto&& entry) {
+ for (auto&& id : ids) {
+ if (entry.th32ProcessID == id) {
+ ps.push_back(Process(
+ entry.th32ProcessID,
+ entry.th32ParentProcessID,
+ QString::fromStdWString(entry.szExeFile)));
+
+ break;
+ }
+ }
+
+ return true;
+ });
+
+ Process root;
+
+ {
+ // getting processes whose parent is not in the list
+ for (auto&& possibleRoot : ps) {
+ const auto ppid = possibleRoot.ppid();
+ bool found = false;
+
+ for (auto&& p : ps) {
+ if (p.pid() == ppid) {
+ found = true;
+ break;
+ }
+ }
+
+ if (!found) {
+ // this is a root process
+ root.addChild(possibleRoot);
+ }
+ }
+
+ // removing root processes from the list
+ auto newEnd = std::remove_if(ps.begin(), ps.end(), [&](auto&& p) {
+ for (auto&& rp : root.children()) {
+ if (rp.pid() == p.pid()) {
+ return true;
+ }
+ }
+
+ return false;
+ });
+
+ ps.erase(newEnd, ps.end());
+ }
+
+ // at this point, `processes` should only contain processes that are direct
+ // or indirect children of the ones in `root`
+
+ if (ps.empty()) {
+ // and that's all there is
+ return root;
+ }
+
+ {
+ // recursively find children
+ for (auto&& r : root.children()) {
+ findChildProcesses(r, ps);
+ }
+ }
+
+ return root;
+}
+
+bool isJobHandle(HANDLE h)
+{
+ JOBOBJECT_BASIC_ACCOUNTING_INFORMATION info = {};
+
+ const auto r = ::QueryInformationJobObject(
+ h, JobObjectBasicAccountingInformation, &info, sizeof(info), nullptr);
+
+ return r;
+}
+
+Process getProcessTree(HANDLE h)
+{
+ if (isJobHandle(h)) {
+ return getProcessTreeFromJob(h);
+ } else {
+ return getProcessTreeFromProcess(h);
+ }
+}
+
+QString getProcessName(DWORD pid)
+{
+ HandlePtr h(OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid));
+
+ if (!h) {
+ const auto e = GetLastError();
+ log::error("can't get name of process {}, {}", pid, formatSystemMessage(e));
+ return {};
+ }
+
+ return getProcessName(h.get());
+}
+
+QString getProcessName(HANDLE process)
+{
+ const QString badName = "unknown";
+
+ if (process == 0 || process == INVALID_HANDLE_VALUE) {
+ return badName;
+ }
+
+ const DWORD bufferSize = MAX_PATH;
+ wchar_t buffer[bufferSize + 1] = {};
+
+ const auto realSize = ::GetProcessImageFileNameW(process, buffer, bufferSize);
+
+ if (realSize == 0) {
+ const auto e = ::GetLastError();
+ log::error("GetProcessImageFileNameW() failed, {}", formatSystemMessage(e));
+ return badName;
+ }
+
+ auto s = QString::fromWCharArray(buffer, realSize);
+
+ const auto lastSlash = s.lastIndexOf("\\");
+ if (lastSlash != -1) {
+ s = s.mid(lastSlash + 1);
+ }
+
+ return s;
+}
+
+DWORD getProcessParentID(DWORD pid)
+{
+ DWORD ppid = 0;
+
+ forEachRunningProcess([&](auto&& entry) {
+ if (entry.th32ProcessID == pid) {
+ ppid = entry.th32ParentProcessID;
+ return false;
+ }
+
+ return true;
+ });
+
+ return ppid;
+}
+
+
+DWORD getProcessParentID(HANDLE handle)
+{
+ return getProcessParentID(GetProcessId(handle));
+}
+
+} // namespace
diff --git a/src/envmodule.h b/src/envmodule.h
new file mode 100644
index 00000000..c2829b32
--- /dev/null
+++ b/src/envmodule.h
@@ -0,0 +1,164 @@
+#ifndef ENV_MODULE_H
+#define ENV_MODULE_H
+
+#include <QString>
+#include <QDateTime>
+
+namespace env
+{
+
+// used by HandlePtr, calls CloseHandle() as the deleter
+//
+struct HandleCloser
+{
+ using pointer = HANDLE;
+
+ void operator()(HANDLE h)
+ {
+ if (h != INVALID_HANDLE_VALUE) {
+ ::CloseHandle(h);
+ }
+ }
+};
+
+using HandlePtr = std::unique_ptr<HANDLE, HandleCloser>;
+
+
+// represents one module
+//
+class Module
+{
+public:
+ Module(QString path, std::size_t fileSize);
+
+ // returns the module's path
+ //
+ const QString& path() const;
+
+ // returns the module's path in lowercase and using forward slashes
+ //
+ QString displayPath() const;
+
+ // returns the size in bytes, may be 0
+ //
+ std::size_t fileSize() const;
+
+ // returns the x.x.x.x version embedded from the version info, may be empty
+ //
+ const QString& version() const;
+
+ // returns the FileVersion entry from the resource file, returns
+ // "(no version)" if not available
+ //
+ const QString& versionString() const;
+
+ // returns the build date from the version info, or the creation time of the
+ // file on the filesystem, may be empty
+ //
+ const QDateTime& timestamp() const;
+
+ // returns the md5 of the file, may be empty for system files
+ //
+ const QString& md5() const;
+
+ // converts timestamp() to a string for display, returns "(no timestamp)" if
+ // not available
+ //
+ QString timestampString() const;
+
+ // returns a string with all the above information on one line
+ //
+ QString toString() const;
+
+private:
+ // contains the information from the version resource
+ //
+ struct FileInfo
+ {
+ VS_FIXEDFILEINFO ffi;
+ QString fileDescription;
+ };
+
+ QString m_path;
+ std::size_t m_fileSize;
+ QString m_version;
+ QDateTime m_timestamp;
+ QString m_versionString;
+ QString m_md5;
+
+ // returns information from the version resource
+ //
+ FileInfo getFileInfo() const;
+
+ // uses VS_FIXEDFILEINFO to build the version string
+ //
+ QString getVersion(const VS_FIXEDFILEINFO& fi) const;
+
+ // uses the file date from VS_FIXEDFILEINFO if available, or gets the
+ // creation date on the file
+ //
+ QDateTime getTimestamp(const VS_FIXEDFILEINFO& fi) const;
+
+ // returns the md5 hash unless the path contains "\windows\"
+ //
+ QString getMD5() const;
+
+ // gets VS_FIXEDFILEINFO from the file version info buffer
+ //
+ VS_FIXEDFILEINFO getFixedFileInfo(std::byte* buffer) const;
+
+ // gets FileVersion from the file version info buffer
+ //
+ QString getFileDescription(std::byte* buffer) const;
+};
+
+
+// represents one process
+//
+class Process
+{
+public:
+ Process();
+ explicit Process(HANDLE h);
+ Process(DWORD pid, DWORD ppid, QString name);
+
+ bool isValid() const;
+ DWORD pid() const;
+ DWORD ppid() const;
+ const QString& name() const;
+
+ HandlePtr openHandleForWait() const;
+
+ // whether this process can be accessed; fails if the current process doesn't
+ // have the proper permissions
+ //
+ bool canAccess() const;
+
+ void addChild(Process p);
+ std::vector<Process>& children();
+ const std::vector<Process>& children() const;
+
+private:
+ DWORD m_pid;
+ mutable std::optional<DWORD> m_ppid;
+ mutable std::optional<QString> m_name;
+ std::vector<Process> m_children;
+};
+
+
+std::vector<Process> getRunningProcesses();
+std::vector<Module> getLoadedModules();
+
+// works for both jobs and processes
+//
+Process getProcessTree(HANDLE h);
+
+QString getProcessName(DWORD pid);
+QString getProcessName(HANDLE process);
+
+DWORD getProcessParentID(DWORD pid);
+DWORD getProcessParentID(HANDLE handle);
+
+} // namespace env
+
+#endif // ENV_MODULE_H
diff --git a/src/envsecurity.cpp b/src/envsecurity.cpp
new file mode 100644
index 00000000..6f4826e7
--- /dev/null
+++ b/src/envsecurity.cpp
@@ -0,0 +1,758 @@
+#include "envsecurity.h"
+#include "env.h"
+#include "envmodule.h"
+#include <utility.h>
+#include <log.h>
+
+#include <Wbemidl.h>
+#include <wscapi.h>
+#include <comdef.h>
+#include <netfw.h>
+#pragma comment(lib, "Wbemuuid.lib")
+
+#include <accctrl.h>
+#include <aclapi.h>
+#include <sddl.h>
+#pragma comment(lib, "advapi32.lib")
+
+namespace env
+{
+
+using namespace MOBase;
+
+class WMI
+{
+public:
+ class failed {};
+
+ WMI(const std::string& ns)
+ {
+ try
+ {
+ createLocator();
+ createService(ns);
+ setSecurity();
+ }
+ catch(failed&)
+ {
+ }
+ }
+
+ template <class F>
+ void query(const std::string& q, F&& f)
+ {
+ if (!m_locator || !m_service) {
+ return;
+ }
+
+ auto enumerator = getEnumerator(q);
+ if (!enumerator) {
+ return;
+ }
+
+ for (;;)
+ {
+ COMPtr<IWbemClassObject> object;
+
+ {
+ IWbemClassObject* rawObject = nullptr;
+ ULONG count = 0;
+ auto ret = enumerator->Next(WBEM_INFINITE, 1, &rawObject, &count);
+
+ if (count == 0 || !rawObject) {
+ break;
+ }
+
+ if (FAILED(ret)) {
+ log::error("enum->next() failed, {}", formatSystemMessage(ret));
+ break;
+ }
+
+ object.reset(rawObject);
+ }
+
+ f(object.get());
+ }
+ }
+
+private:
+ COMPtr<IWbemLocator> m_locator;
+ COMPtr<IWbemServices> m_service;
+
+ void createLocator()
+ {
+ void* rawLocator = nullptr;
+
+ const auto ret = CoCreateInstance(
+ CLSID_WbemLocator, nullptr, CLSCTX_INPROC_SERVER,
+ IID_IWbemLocator, &rawLocator);
+
+ if (FAILED(ret) || !rawLocator) {
+ log::error(
+ "CoCreateInstance for WbemLocator failed, {}",
+ formatSystemMessage(ret));
+
+ throw failed();
+ }
+
+ m_locator.reset(static_cast<IWbemLocator*>(rawLocator));
+ }
+
+ void createService(const std::string& ns)
+ {
+ IWbemServices* rawService = nullptr;
+
+ const auto res = m_locator->ConnectServer(
+ _bstr_t(ns.c_str()),
+ nullptr, nullptr, nullptr, 0, nullptr, nullptr,
+ &rawService);
+
+ if (FAILED(res) || !rawService) {
+ log::error(
+ "locator->ConnectServer() failed for namespace '{}', {}",
+ ns, formatSystemMessage(res));
+
+ throw failed();
+ }
+
+ m_service.reset(rawService);
+ }
+
+ void setSecurity()
+ {
+ auto ret = CoSetProxyBlanket(
+ m_service.get(), RPC_C_AUTHN_WINNT, RPC_C_AUTHZ_NONE, nullptr,
+ RPC_C_AUTHN_LEVEL_CALL, RPC_C_IMP_LEVEL_IMPERSONATE, 0, EOAC_NONE);
+
+ if (FAILED(ret))
+ {
+ log::error("CoSetProxyBlanket() failed, {}", formatSystemMessage(ret));
+ throw failed();
+ }
+ }
+
+ COMPtr<IEnumWbemClassObject> getEnumerator(
+ const std::string& query)
+ {
+ IEnumWbemClassObject* rawEnumerator = NULL;
+
+ auto ret = m_service->ExecQuery(
+ bstr_t("WQL"),
+ bstr_t(query.c_str()),
+ WBEM_FLAG_FORWARD_ONLY | WBEM_FLAG_RETURN_IMMEDIATELY,
+ NULL,
+ &rawEnumerator);
+
+ if (FAILED(ret) || !rawEnumerator)
+ {
+ log::error("query '{}' failed, {}", query, formatSystemMessage(ret));
+ return {};
+ }
+
+ return COMPtr<IEnumWbemClassObject>(rawEnumerator);
+ }
+};
+
+
+SecurityProduct::SecurityProduct(
+ QUuid guid, QString name, int provider,
+ bool active, bool upToDate) :
+ m_guid(std::move(guid)), m_name(std::move(name)), m_provider(provider),
+ m_active(active), m_upToDate(upToDate)
+{
+}
+
+const QUuid& SecurityProduct::guid() const
+{
+ return m_guid;
+}
+
+const QString& SecurityProduct::name() const
+{
+ return m_name;
+}
+
+int SecurityProduct::provider() const
+{
+ return m_provider;
+}
+
+bool SecurityProduct::active() const
+{
+ return m_active;
+}
+
+bool SecurityProduct::upToDate() const
+{
+ return m_upToDate;
+}
+
+QString SecurityProduct::toString() const
+{
+ QString s;
+
+ if (m_name.isEmpty()) {
+ s += "(no name)";
+ } else {
+ s += m_name;
+ }
+
+ s += " (" + providerToString() + ")";
+
+ if (!m_active) {
+ s += ", inactive";
+ }
+
+ if (!m_upToDate) {
+ s += ", definitions outdated";
+ }
+
+ return s;
+}
+
+QString SecurityProduct::providerToString() const
+{
+ QStringList ps;
+
+ if (m_provider & WSC_SECURITY_PROVIDER_FIREWALL) {
+ ps.push_back("firewall");
+ }
+
+ if (m_provider & WSC_SECURITY_PROVIDER_AUTOUPDATE_SETTINGS) {
+ ps.push_back("autoupdate");
+ }
+
+ if (m_provider & WSC_SECURITY_PROVIDER_ANTIVIRUS) {
+ ps.push_back("antivirus");
+ }
+
+ if (m_provider & WSC_SECURITY_PROVIDER_ANTISPYWARE) {
+ ps.push_back("antispyware");
+ }
+
+ if (m_provider & WSC_SECURITY_PROVIDER_INTERNET_SETTINGS) {
+ ps.push_back("settings");
+ }
+
+ if (m_provider & WSC_SECURITY_PROVIDER_USER_ACCOUNT_CONTROL) {
+ ps.push_back("uac");
+ }
+
+ if (m_provider & WSC_SECURITY_PROVIDER_SERVICE) {
+ ps.push_back("service");
+ }
+
+ if (ps.empty()) {
+ return "doesn't provider anything";
+ }
+
+ return ps.join("|");
+}
+
+
+std::optional<SecurityProduct> handleProduct(IWbemClassObject* o)
+{
+ VARIANT prop;
+
+
+ // guid
+ auto ret = o->Get(L"instanceGuid", 0, &prop, 0, 0);
+ if (FAILED(ret)) {
+ log::error("failed to get instanceGuid, {}", formatSystemMessage(ret));
+ return {};
+ }
+
+ if (prop.vt != VT_BSTR) {
+ log::error("instanceGuid is a {}, not a bstr", prop.vt);
+ return {};
+ }
+
+ const QUuid guid(QString::fromWCharArray(prop.bstrVal));
+ VariantClear(&prop);
+
+
+ // display name
+ QString displayName;
+ ret = o->Get(L"displayName", 0, &prop, 0, 0);
+
+ if (FAILED(ret)) {
+ log::error("failed to get displayName, {}", formatSystemMessage(ret));
+ } else if (prop.vt != VT_BSTR) {
+ log::error("displayName is a {}, not a bstr", prop.vt);
+ } else {
+ displayName = QString::fromWCharArray(prop.bstrVal);
+ }
+
+ VariantClear(&prop);
+
+
+ // product state
+ DWORD state = 0;
+ ret = o->Get(L"productState", 0, &prop, 0, 0);
+
+ if (FAILED(ret)) {
+ log::error("failed to get productState, {}", formatSystemMessage(ret));
+ } else {
+ if (prop.vt == VT_I4) {
+ state = prop.lVal;
+ } else if (prop.vt == VT_UI4) {
+ state = prop.ulVal;
+ } else if (prop.vt == VT_NULL) {
+ log::warn("productState is null");
+ } else {
+ log::error("productState is a {}, not a VT_I4 or a VT_UI4", prop.vt);
+ }
+ }
+
+ VariantClear(&prop);
+
+
+ const auto provider = static_cast<int>((state >> 16) & 0xff);
+ const auto scanner = (state >> 8) & 0xff;
+ const auto definitions = state & 0xff;
+
+ const bool active = ((scanner & 0x10) != 0);
+ const bool upToDate = (definitions == 0);
+
+ return SecurityProduct(guid, displayName, provider, active, upToDate);
+}
+
+std::vector<SecurityProduct> getSecurityProductsFromWMI()
+{
+ // some products may be present in multiple queries, such as a product marked
+ // as both antivirus and antispyware, but they'll have the same GUID, so use
+ // that to avoid duplicating entries
+ std::map<QUuid, SecurityProduct> map;
+
+ auto f = [&](auto* o) {
+ if (auto p=handleProduct(o)) {
+ map.emplace(p->guid(), std::move(*p));
+ }
+ };
+
+ {
+ WMI wmi("root\\SecurityCenter2");
+ wmi.query("select * from AntivirusProduct", f);
+ wmi.query("select * from FirewallProduct", f);
+ wmi.query("select * from AntiSpywareProduct", f);
+ }
+
+ {
+ WMI wmi("root\\SecurityCenter");
+ wmi.query("select * from AntivirusProduct", f);
+ wmi.query("select * from FirewallProduct", f);
+ wmi.query("select * from AntiSpywareProduct", f);
+ }
+
+ std::vector<SecurityProduct> v;
+
+ for (auto&& p : map) {
+ v.push_back(p.second);
+ }
+
+ return v;
+}
+
+std::optional<SecurityProduct> getWindowsFirewall()
+{
+ HRESULT hr = 0;
+
+ COMPtr<INetFwPolicy2> policy;
+
+ {
+ void* rawPolicy = nullptr;
+
+ hr = CoCreateInstance(
+ __uuidof(NetFwPolicy2), nullptr, CLSCTX_INPROC_SERVER,
+ __uuidof(INetFwPolicy2), &rawPolicy);
+
+ if (FAILED(hr) || !rawPolicy) {
+ log::error(
+ "CoCreateInstance for NetFwPolicy2 failed, {}",
+ formatSystemMessage(hr));
+
+ return {};
+ }
+
+ policy.reset(static_cast<INetFwPolicy2*>(rawPolicy));
+ }
+
+ VARIANT_BOOL enabledVariant;
+
+ if (policy) {
+ hr = policy->get_FirewallEnabled(NET_FW_PROFILE2_PUBLIC, &enabledVariant);
+ if (FAILED(hr))
+ {
+ // EPT_S_NOT_REGISTERED is "There are no more endpoints available from the
+ // endpoint mapper", which seems to happen sometimes on Windows 7 when the
+ // firewall has been disabled, so treat it as such and don't log it
+ //
+ // however the user reported the error was actually 0x800706d9, not just
+ // 0x6d9 (1753, what EPT_S_NOT_REGISTERED is defined to), so this is
+ // testing for both because it's not clear which it is and nobody can
+ // reproduce it
+ if (hr != EPT_S_NOT_REGISTERED && hr != 0x800706d9) {
+ log::error("get_FirewallEnabled failed, {}", formatSystemMessage(hr));
+ }
+
+ return {};
+ }
+ }
+
+ const auto enabled = (enabledVariant != VARIANT_FALSE);
+ if (!enabled) {
+ return {};
+ }
+
+ return SecurityProduct(
+ {}, "Windows Firewall", WSC_SECURITY_PROVIDER_FIREWALL, true, true);
+}
+
+
+std::vector<SecurityProduct> getSecurityProducts()
+{
+ std::vector<SecurityProduct> v;
+
+ {
+ auto fromWMI = getSecurityProductsFromWMI();
+ v.insert(
+ v.end(),
+ std::make_move_iterator(fromWMI.begin()),
+ std::make_move_iterator(fromWMI.end()));
+ }
+
+ if (auto p=getWindowsFirewall()) {
+ v.push_back(std::move(*p));
+ }
+
+ return v;
+}
+
+
+class failed
+{
+public:
+ failed(DWORD e, QString what)
+ : m_what(what + ", " + QString::fromStdWString(formatSystemMessage(e)))
+ {
+ }
+
+ QString what() const
+ {
+ return m_what;
+ }
+
+private:
+ QString m_what;
+};
+
+
+MallocPtr<SECURITY_DESCRIPTOR> getSecurityDescriptor(const QString& path)
+{
+ const auto wpath = path.toStdWString();
+ BOOL ret = FALSE;
+
+ DWORD length = 0;
+ ret = ::GetFileSecurityW(
+ wpath.c_str(), DACL_SECURITY_INFORMATION|OWNER_SECURITY_INFORMATION,
+ nullptr, 0, &length);
+
+ if (!ret || length == 0) {
+ const auto e = GetLastError();
+
+ if (e != ERROR_INSUFFICIENT_BUFFER) {
+ if (e == ERROR_ACCESS_DENIED) {
+ // if this fails, the user doesn't even have permissions to get the
+ // security descriptor, which probably means they're not the owner and
+ // their effective access is none
+ throw failed(e, "cannot get security descriptor");
+ } else {
+ // other error
+ throw failed(e, "GetFileSecurity() for length failed");
+ }
+ }
+ }
+
+ MallocPtr<SECURITY_DESCRIPTOR> sd(
+ static_cast<SECURITY_DESCRIPTOR*>(std::malloc(length)));
+
+ std::memset(sd.get(), 0, length);
+
+ ret = ::GetFileSecurityW(
+ wpath.c_str(), DACL_SECURITY_INFORMATION|OWNER_SECURITY_INFORMATION,
+ sd.get(), length, &length);
+
+ if (!ret) {
+ const auto e = GetLastError();
+ throw failed(e, "GetFileSecurity()");
+ }
+
+ return sd;
+}
+
+PACL getDacl(SECURITY_DESCRIPTOR* sd)
+{
+ BOOL present = FALSE;
+ BOOL daclDefaulted = FALSE;
+ PACL acl = nullptr;
+
+ BOOL ret = ::GetSecurityDescriptorDacl(sd, &present, &acl, &daclDefaulted);
+
+ if (!ret) {
+ const auto e = GetLastError();
+ throw failed(e, "GetSecurityDescriptorDacl()");
+ }
+
+ if (!present) {
+ return nullptr;
+ }
+
+ return acl;
+}
+
+PSID getFileOwner(SECURITY_DESCRIPTOR* sd)
+{
+ BOOL ownerDefaulted = FALSE;
+ PSID owner;
+
+ BOOL ret = ::GetSecurityDescriptorOwner(sd, &owner, &ownerDefaulted);
+
+ if (!ret) {
+ const auto e = GetLastError();
+ throw failed(e, "GetSecurityDescriptionOwner()");
+ }
+
+ return owner;
+}
+
+MallocPtr<void> getCurrentUser()
+{
+ HANDLE hnd = ::GetCurrentProcess();
+ HANDLE rawToken = 0;
+
+ BOOL ret = ::OpenProcessToken(hnd, TOKEN_QUERY, &rawToken);
+ if (!ret) {
+ const auto e = GetLastError();
+ throw(e, "OpenProcessToken()");
+ }
+
+ HandlePtr token(rawToken);
+
+ DWORD retsize = 0;
+ ret = ::GetTokenInformation(token.get(), TokenUser, 0, 0, &retsize);
+
+ if (!ret) {
+ const auto e = GetLastError();
+ if (e != ERROR_INSUFFICIENT_BUFFER) {
+ throw failed(e, "GetTokenInformation() for length");
+ }
+ }
+
+ MallocPtr<void> tokenBuffer(std::malloc(retsize));
+ ret = ::GetTokenInformation(
+ token.get(), TokenUser, tokenBuffer.get(), retsize, &retsize);
+
+ if (!ret) {
+ const auto e = GetLastError();
+ throw failed(e, "GetTokenInformation()");
+ }
+
+ PSID tokenSid = ((PTOKEN_USER)(tokenBuffer.get()))->User.Sid;
+ DWORD sidLen = ::GetLengthSid(tokenSid);
+ MallocPtr<void> currentUserSID((SID*)(malloc(sidLen)));
+
+ ret = ::CopySid(sidLen, currentUserSID.get(), tokenSid);
+
+ if (!ret) {
+ const auto e = GetLastError();
+ throw failed(e, "CopySid()");
+ }
+
+ return currentUserSID;
+}
+
+ACCESS_MASK getEffectiveRights(ACL* dacl, PSID sid)
+{
+ TRUSTEEW trustee = {};
+ BuildTrusteeWithSid(&trustee, sid);
+
+ ACCESS_MASK access = 0;
+ DWORD ret = ::GetEffectiveRightsFromAclW(dacl, &trustee, &access);
+
+ if (ret != ERROR_SUCCESS) {
+ throw failed(ret, "GetEffectiveRightsFromAclW()");
+ }
+
+ return access;
+}
+
+QString getUsername(PSID owner)
+{
+ DWORD nameSize=0, domainSize=0;
+ auto use = SidTypeUnknown;
+
+ BOOL ret = LookupAccountSidW(
+ nullptr, owner, nullptr, &nameSize, nullptr, &domainSize, &use);
+
+ if (!ret) {
+ const auto e = GetLastError();
+
+ if (e != ERROR_INSUFFICIENT_BUFFER) {
+ throw failed(e, "LookupAccountSid() for sizes");
+ }
+ }
+
+ auto wsName = std::make_unique<wchar_t[]>(nameSize);
+ auto wsDomain = std::make_unique<wchar_t[]>(domainSize);
+
+ ret = LookupAccountSidW(
+ nullptr, owner, wsName.get(), &nameSize, wsDomain.get(), &domainSize, &use);
+
+ if (!ret) {
+ const auto e = GetLastError();
+ throw failed(e, "LookupAccountSid()");
+ }
+
+ const QString name = QString::fromWCharArray(wsName.get(), nameSize);
+ const QString domain = QString::fromWCharArray(wsDomain.get(), domainSize);
+
+ if (!name.isEmpty() && !domain.isEmpty()) {
+ return domain + "\\" + name;
+ } else {
+ // either or both are empty
+ return name + domain;
+ }
+}
+
+FileRights makeFileRights(ACCESS_MASK m)
+{
+ FileRights fr;
+
+ if (m & FILE_GENERIC_READ) {
+ fr.list.push_back("file_generic_read");
+ } else {
+ if (m & READ_CONTROL) {
+ fr.list.push_back("read_ctrl");
+ }
+
+ if (m & FILE_READ_DATA) {
+ fr.list.push_back("read_data");
+ }
+
+ if (m & FILE_READ_ATTRIBUTES) {
+ fr.list.push_back("read_atts");
+ }
+
+ if (m & FILE_READ_EA) {
+ fr.list.push_back("read_ex_atts");
+ }
+
+ if (m & SYNCHRONIZE) {
+ fr.list.push_back("sync");
+ }
+ }
+
+ if (m & FILE_GENERIC_WRITE) {
+ fr.list.push_back("file_generic_write");
+ } else {
+ // READ_CONTROL handled above
+
+ if (m & FILE_WRITE_DATA) {
+ fr.list.push_back("write_data");
+ }
+
+ if (m & FILE_WRITE_ATTRIBUTES) {
+ fr.list.push_back("write_atts");
+ }
+
+ if (m & FILE_WRITE_EA) {
+ fr.list.push_back("write_ex_atts");
+ }
+
+ if (m & FILE_APPEND_DATA) {
+ fr.list.push_back("append_data");
+ }
+
+ // SYNCHRONIZE handled above
+ }
+
+ if (m & FILE_GENERIC_EXECUTE) {
+ fr.list.push_back("file_generic_execute");
+ fr.hasExecute = true;
+ } else {
+ // READ_CONTROL handled above
+ // FILE_READ_ATTRIBUTES handled above
+
+ if (m & FILE_EXECUTE) {
+ fr.list.push_back("execute");
+ fr.hasExecute = true;
+ }
+
+ // SYNCHRONIZE handled above
+ }
+
+ if (m & DELETE) {
+ fr.list.push_back("delete");
+ }
+
+ if (m & WRITE_DAC) {
+ fr.list.push_back("write_dac");
+ }
+
+ if (m & WRITE_OWNER) {
+ fr.list.push_back("write_owner");
+ }
+
+ if (m & GENERIC_ALL) {
+ fr.list.push_back("generic_all");
+ }
+
+ if (m & GENERIC_WRITE) {
+ fr.list.push_back("generic_write");
+ }
+
+ if (m & GENERIC_READ) {
+ fr.list.push_back("generic_read");
+ }
+
+ // 0x001f01ff
+ const auto normalRights =
+ STANDARD_RIGHTS_ALL |
+ FILE_GENERIC_READ | FILE_GENERIC_WRITE | FILE_GENERIC_EXECUTE |
+ FILE_DELETE_CHILD;
+
+ if (m == normalRights) {
+ fr.normalRights = true;
+ }
+
+ return fr;
+}
+
+FileSecurity getFileSecurity(const QString& path)
+{
+ FileSecurity fs;
+
+ try
+ {
+ auto sd = getSecurityDescriptor(path);
+ auto dacl = getDacl(sd.get());
+ auto currentUser = getCurrentUser();
+ auto owner = getFileOwner(sd.get());
+ auto access = getEffectiveRights(dacl, currentUser.get());
+
+ fs.rights = makeFileRights(access);
+
+ if (EqualSid(owner, currentUser.get())) {
+ fs.owner = "(this user)";
+ } else {
+ fs.owner = getUsername(owner);
+ }
+ }
+ catch(failed& f)
+ {
+ fs.error = f.what();
+ }
+
+ return fs;
+}
+} // namespace
diff --git a/src/envsecurity.h b/src/envsecurity.h
new file mode 100644
index 00000000..5f9e5332
--- /dev/null
+++ b/src/envsecurity.h
@@ -0,0 +1,75 @@
+#ifndef ENV_SECURITY_H
+#define ENV_SECURITY_H
+
+#include <QUuid>
+#include <QString>
+
+namespace env
+{
+
+// represents a security product, such as an antivirus or a firewall
+//
+class SecurityProduct
+{
+public:
+ SecurityProduct(
+ QUuid guid, QString name, int provider,
+ bool active, bool upToDate);
+
+ // guid
+ //
+ const QUuid& guid() const;
+
+ // display name of the product
+ //
+ const QString& name() const;
+
+ // a bunch of _WSC_SECURITY_PROVIDER flags
+ //
+ int provider() const;
+
+ // whether the product is active
+ //
+ bool active() const;
+
+ // whether its definitions are up-to-date
+ //
+ bool upToDate() const;
+
+ // string representation of the above
+ //
+ QString toString() const;
+
+private:
+ QUuid m_guid;
+ QString m_name;
+ int m_provider;
+ bool m_active;
+ bool m_upToDate;
+
+ QString providerToString() const;
+};
+
+
+std::vector<SecurityProduct> getSecurityProducts();
+
+
+struct FileRights
+{
+ QStringList list;
+ bool hasExecute = false;
+ bool normalRights = false;
+};
+
+struct FileSecurity
+{
+ QString owner;
+ FileRights rights;
+ QString error;
+};
+
+FileSecurity getFileSecurity(const QString& file);
+
+} // namespace env
+
+#endif // ENV_SECURITY_H
diff --git a/src/envshortcut.cpp b/src/envshortcut.cpp
new file mode 100644
index 00000000..99495c39
--- /dev/null
+++ b/src/envshortcut.cpp
@@ -0,0 +1,374 @@
+#include "envshortcut.h"
+#include "env.h"
+#include "executableslist.h"
+#include "instancemanager.h"
+#include <utility.h>
+#include <log.h>
+
+namespace env
+{
+
+using namespace MOBase;
+
+class ShellLinkException
+{
+public:
+ ShellLinkException(QString s)
+ : m_what(std::move(s))
+ {
+ }
+
+ const QString& what() const
+ {
+ return m_what;
+ }
+
+private:
+ QString m_what;
+};
+
+// just a wrapper around IShellLink operations that throws ShellLinkException
+// on errors
+//
+class ShellLinkWrapper
+{
+public:
+ ShellLinkWrapper()
+ {
+ m_link = createShellLink();
+ m_file = createPersistFile();
+ }
+
+ void setPath(const QString& s)
+ {
+ if (s.isEmpty()) {
+ throw ShellLinkException("path cannot be empty");
+ }
+
+ const auto r = m_link->SetPath(s.toStdWString().c_str());
+ throwOnFail(r, QString("failed to set target path '%1'").arg(s));
+ }
+
+ void setArguments(const QString& s)
+ {
+ const auto r = m_link->SetArguments(s.toStdWString().c_str());
+ throwOnFail(r, QString("failed to set arguments '%1'").arg(s));
+ }
+
+ void setDescription(const QString& s)
+ {
+ if (s.isEmpty()) {
+ return;
+ }
+
+ const auto r = m_link->SetDescription(s.toStdWString().c_str());
+ throwOnFail(r, QString("failed to set description '%1'").arg(s));
+ }
+
+ void setIcon(const QString& file, int i)
+ {
+ if (file.isEmpty()) {
+ return;
+ }
+
+ const auto r = m_link->SetIconLocation(file.toStdWString().c_str(), i);
+ throwOnFail(r, QString("failed to set icon '%1' @ %2").arg(file).arg(i));
+ }
+
+ void setWorkingDirectory(const QString& s)
+ {
+ if (s.isEmpty()) {
+ return;
+ }
+
+ const auto r = m_link->SetWorkingDirectory(s.toStdWString().c_str());
+ throwOnFail(r, QString("failed to set working directory '%1'").arg(s));
+ }
+
+ void save(const QString& path)
+ {
+ const auto r = m_file->Save(path.toStdWString().c_str(), TRUE);
+ throwOnFail(r, QString("failed to save link '%1'").arg(path));
+ }
+
+private:
+ COMPtr<IShellLink> m_link;
+ COMPtr<IPersistFile> m_file;
+
+ void throwOnFail(HRESULT r, const QString& s)
+ {
+ if (FAILED(r)) {
+ throw ShellLinkException(QString("%1, %2")
+ .arg(s)
+ .arg(formatSystemMessage(r)));
+ }
+ }
+
+ COMPtr<IShellLink> createShellLink()
+ {
+ void* link = nullptr;
+
+ const auto r = CoCreateInstance(
+ CLSID_ShellLink, nullptr, CLSCTX_INPROC_SERVER,
+ IID_IShellLink, &link);
+
+ throwOnFail(r, "failed to create IShellLink instance");
+
+ if (!link) {
+ throw ShellLinkException("creating IShellLink worked, pointer is null");
+ }
+
+ return COMPtr<IShellLink>(static_cast<IShellLink*>(link));
+ }
+
+ COMPtr<IPersistFile> createPersistFile()
+ {
+ void* file = nullptr;
+
+ const auto r = m_link->QueryInterface(IID_IPersistFile, &file);
+ throwOnFail(r, "failed to get IPersistFile interface");
+
+ if (!file) {
+ throw ShellLinkException("querying IPersistFile worked, pointer is null");
+ }
+
+ return COMPtr<IPersistFile>(static_cast<IPersistFile*>(file));
+ }
+};
+
+
+Shortcut::Shortcut()
+ : m_iconIndex(0)
+{
+}
+
+Shortcut::Shortcut(const Executable& exe)
+ : Shortcut()
+{
+ m_name = exe.title();
+ m_target = QFileInfo(qApp->applicationFilePath()).absoluteFilePath();
+
+ m_arguments = QString("\"moshortcut://%1:%2\"")
+ .arg(InstanceManager::instance().currentInstance())
+ .arg(exe.title());
+
+ m_description = QString("Run %1 with ModOrganizer").arg(exe.title());
+
+ if (exe.usesOwnIcon()) {
+ m_icon = exe.binaryInfo().absoluteFilePath();
+ }
+
+ m_workingDirectory = qApp->applicationDirPath();
+}
+
+Shortcut& Shortcut::name(const QString& s)
+{
+ m_name = s;
+ return *this;
+}
+
+Shortcut& Shortcut::target(const QString& s)
+{
+ m_target = s;
+ return *this;
+}
+
+Shortcut& Shortcut::arguments(const QString& s)
+{
+ m_arguments = s;
+ return *this;
+}
+
+Shortcut& Shortcut::description(const QString& s)
+{
+ m_description = s;
+ return *this;
+}
+
+Shortcut& Shortcut::icon(const QString& s, int index)
+{
+ m_icon = s;
+ m_iconIndex = index;
+ return *this;
+}
+
+Shortcut& Shortcut::workingDirectory(const QString& s)
+{
+ m_workingDirectory = s;
+ return *this;
+}
+
+bool Shortcut::exists(Locations loc) const
+{
+ const auto path = shortcutPath(loc);
+ if (path.isEmpty()) {
+ return false;
+ }
+
+ return QFileInfo(path).exists();
+}
+
+bool Shortcut::toggle(Locations loc)
+{
+ if (exists(loc)) {
+ return remove(loc);
+ } else {
+ return add(loc);
+ }
+}
+
+bool Shortcut::add(Locations loc)
+{
+ log::debug(
+ "adding shortcut to {}:\n"
+ " . name: '{}'\n"
+ " . target: '{}'\n"
+ " . arguments: '{}'\n"
+ " . description: '{}'\n"
+ " . icon: '{}' @ {}\n"
+ " . working directory: '{}'",
+ toString(loc),
+ m_name,
+ m_target,
+ m_arguments,
+ m_description,
+ m_icon, m_iconIndex,
+ m_workingDirectory);
+
+ if (m_target.isEmpty()) {
+ log::error("shortcut: target is empty");
+ return false;
+ }
+
+ const auto path = shortcutPath(loc);
+ if (path.isEmpty()) {
+ return false;
+ }
+
+ log::debug("shorcut file will be saved at '{}'", path);
+
+ try
+ {
+ ShellLinkWrapper link;
+
+ link.setPath(m_target);
+ link.setArguments(m_arguments);
+ link.setDescription(m_description);
+ link.setIcon(m_icon, m_iconIndex);
+ link.setWorkingDirectory(m_workingDirectory);
+
+ link.save(path);
+
+ return true;
+ }
+ catch(ShellLinkException& e)
+ {
+ log::error("{}\nshortcut file was not saved", e.what());
+ }
+
+ return false;
+}
+
+bool Shortcut::remove(Locations loc)
+{
+ log::debug("removing shortcut for '{}' from {}", m_name, toString(loc));
+
+ const auto path = shortcutPath(loc);
+ if (path.isEmpty()) {
+ return false;
+ }
+
+ log::debug("path to shortcut file is '{}'", path);
+
+ if (!QFile::exists(path)) {
+ log::error("can't remove shortcut '{}', file not found", path);
+ return false;
+ }
+
+ if (!MOBase::shellDelete({path})) {
+ const auto e = ::GetLastError();
+
+ log::error(
+ "failed to remove shortcut '{}', {}",
+ path, formatSystemMessage(e));
+
+ return false;
+ }
+
+ return true;
+}
+
+QString Shortcut::shortcutPath(Locations loc) const
+{
+ const auto dir = shortcutDirectory(loc);
+ if (dir.isEmpty()) {
+ return {};
+ }
+
+ const auto file = shortcutFilename();
+ if (file.isEmpty()) {
+ return {};
+ }
+
+ return dir + QDir::separator() + file;
+}
+
+QString Shortcut::shortcutDirectory(Locations loc) const
+{
+ QString dir;
+
+ try
+ {
+ switch (loc)
+ {
+ case Desktop:
+ dir = MOBase::getDesktopDirectory();
+ break;
+
+ case StartMenu:
+ dir = MOBase::getStartMenuDirectory();
+ break;
+
+ case None:
+ default:
+ log::error("shortcut: bad location {}", loc);
+ break;
+ }
+ }
+ catch(std::exception&)
+ {
+ }
+
+ return QDir::toNativeSeparators(dir);
+}
+
+QString Shortcut::shortcutFilename() const
+{
+ if (m_name.isEmpty()) {
+ log::error("shortcut name is empty");
+ return {};
+ }
+
+ return m_name + ".lnk";
+}
+
+
+QString toString(Shortcut::Locations loc)
+{
+ switch (loc)
+ {
+ case Shortcut::None:
+ return "none";
+
+ case Shortcut::Desktop:
+ return "desktop";
+
+ case Shortcut::StartMenu:
+ return "start menu";
+
+ default:
+ return QString("? (%1)").arg(static_cast<int>(loc));
+ }
+}
+
+} // namespace
diff --git a/src/envshortcut.h b/src/envshortcut.h
new file mode 100644
index 00000000..a05528d9
--- /dev/null
+++ b/src/envshortcut.h
@@ -0,0 +1,110 @@
+#ifndef ENV_SHORTCUT_H
+#define ENV_SHORTCUT_H
+
+#include <QString>
+
+class Executable;
+
+namespace env
+{
+
+// an application shortcut that can be either on the desktop or the start menu
+//
+class Shortcut
+{
+public:
+ // location of a shortcut
+ //
+ enum Locations
+ {
+ None = 0,
+
+ // on the desktop
+ Desktop,
+
+ // in the start menu
+ StartMenu
+ };
+
+
+ // empty shortcut
+ //
+ Shortcut();
+
+ // shortcut from an executable
+ //
+ explicit Shortcut(const Executable& exe);
+
+ // sets the name of the shortcut, shown on icons and start menu entries
+ //
+ Shortcut& name(const QString& s);
+
+ // the program to start
+ //
+ Shortcut& target(const QString& s);
+
+ // arguments to pass
+ //
+ Shortcut& arguments(const QString& s);
+
+ // shows in the status bar of explorer, for example
+ //
+ Shortcut& description(const QString& s);
+
+ // path to a binary that contains the icon and its index
+ //
+ Shortcut& icon(const QString& s, int index=0);
+
+ // "start in" option for this shortcut
+ //
+ Shortcut& workingDirectory(const QString& s);
+
+
+ // returns whether this shortcut already exists at the given location; this
+ // does not check whether the shortcut parameters are different, it merely if
+ // the .lnk file exists
+ //
+ bool exists(Locations loc) const;
+
+ // calls remove() if exists(), or add()
+ //
+ bool toggle(Locations loc);
+
+ // adds the shortcut to the given location
+ //
+ bool add(Locations loc);
+
+ // removes the shortcut from the given location
+ //
+ bool remove(Locations loc);
+
+private:
+ QString m_name;
+ QString m_target;
+ QString m_arguments;
+ QString m_description;
+ QString m_icon;
+ int m_iconIndex;
+ QString m_workingDirectory;
+
+ // returns the path where the shortcut file should be saved
+ //
+ QString shortcutPath(Locations loc) const;
+
+ // returns the directory where the shortcut file should be saved
+ //
+ QString shortcutDirectory(Locations loc) const;
+
+ // returns the filename of the shortcut file that should be used when saving
+ //
+ QString shortcutFilename() const;
+};
+
+
+// returns a string representation of the given location
+//
+QString toString(Shortcut::Locations loc);
+
+} // namespace
+
+#endif // ENV_SHORTCUT_H
diff --git a/src/envwindows.cpp b/src/envwindows.cpp
new file mode 100644
index 00000000..98e78a3e
--- /dev/null
+++ b/src/envwindows.cpp
@@ -0,0 +1,238 @@
+#include "envwindows.h"
+#include "env.h"
+#include "envmodule.h"
+#include <utility.h>
+#include <log.h>
+
+namespace env
+{
+
+using namespace MOBase;
+
+WindowsInfo::WindowsInfo()
+{
+ // loading ntdll.dll, the functions will be found with GetProcAddress()
+ LibraryPtr ntdll(LoadLibraryW(L"ntdll.dll"));
+
+ if (!ntdll) {
+ log::error("failed to load ntdll.dll while getting version");
+ return;
+ } else {
+ m_reported = getReportedVersion(ntdll.get());
+ m_real = getRealVersion(ntdll.get());
+ }
+
+ m_release = getRelease();
+ m_elevated = getElevated();
+}
+
+bool WindowsInfo::compatibilityMode() const
+{
+ if (m_real == Version()) {
+ // don't know the real version, can't guess compatibility mode
+ return false;
+ }
+
+ return (m_real != m_reported);
+}
+
+const WindowsInfo::Version& WindowsInfo::reportedVersion() const
+{
+ return m_reported;
+}
+
+const WindowsInfo::Version& WindowsInfo::realVersion() const
+{
+ return m_real;
+}
+
+const WindowsInfo::Release& WindowsInfo::release() const
+{
+ return m_release;
+}
+
+std::optional<bool> WindowsInfo::isElevated() const
+{
+ return m_elevated;
+}
+
+QString WindowsInfo::toString() const
+{
+ QStringList sl;
+
+ const QString reported = m_reported.toString();
+ const QString real = m_real.toString();
+
+ // version
+ sl.push_back("version " + reported);
+
+ // real version if different
+ if (compatibilityMode()) {
+ sl.push_back("real version " + real);
+ }
+
+ // build.UBR, such as 17763.557
+ if (m_release.UBR != 0) {
+ DWORD build = 0;
+
+ if (compatibilityMode()) {
+ build = m_real.build;
+ } else {
+ build = m_reported.build;
+ }
+
+ sl.push_back(QString("%1.%2").arg(build).arg(m_release.UBR));
+ }
+
+ // release ID
+ if (!m_release.ID.isEmpty()) {
+ sl.push_back("release " + m_release.ID);
+ }
+
+ // buildlab string
+ if (!m_release.buildLab.isEmpty()) {
+ sl.push_back(m_release.buildLab);
+ }
+
+ // product name
+ if (!m_release.productName.isEmpty()) {
+ sl.push_back(m_release.productName);
+ }
+
+ // elevated
+ QString elevated = "?";
+ if (m_elevated.has_value()) {
+ elevated = (*m_elevated ? "yes" : "no");
+ }
+
+ sl.push_back("elevated: " + elevated);
+
+ return sl.join(", ");
+}
+
+WindowsInfo::Version WindowsInfo::getReportedVersion(HINSTANCE ntdll) const
+{
+ // windows has been deprecating pretty much all the functions having to do
+ // with getting version information because apparently, people keep misusing
+ // them for feature detection
+ //
+ // there's still RtlGetVersion() though
+
+ using RtlGetVersionType = NTSTATUS (NTAPI)(PRTL_OSVERSIONINFOW);
+
+ auto* RtlGetVersion = reinterpret_cast<RtlGetVersionType*>(
+ GetProcAddress(ntdll, "RtlGetVersion"));
+
+ if (!RtlGetVersion) {
+ log::error("RtlGetVersion() not found in ntdll.dll");
+ return {};
+ }
+
+ OSVERSIONINFOEX vi = {};
+ vi.dwOSVersionInfoSize = sizeof(vi);
+
+ // this apparently never fails
+ RtlGetVersion((RTL_OSVERSIONINFOW*)&vi);
+
+ return {vi.dwMajorVersion, vi.dwMinorVersion, vi.dwBuildNumber};
+}
+
+WindowsInfo::Version WindowsInfo::getRealVersion(HINSTANCE ntdll) const
+{
+ // getting the actual windows version is more difficult because all the
+ // functions are lying when running in compatibility mode
+ //
+ // RtlGetNtVersionNumbers() is an undocumented function that seems to work
+ // fine, but it might not in the future
+
+ using RtlGetNtVersionNumbersType = void (NTAPI)(DWORD*, DWORD*, DWORD*);
+
+ auto* RtlGetNtVersionNumbers = reinterpret_cast<RtlGetNtVersionNumbersType*>(
+ GetProcAddress(ntdll, "RtlGetNtVersionNumbers"));
+
+ if (!RtlGetNtVersionNumbers) {
+ log::error("RtlGetNtVersionNumbers not found in ntdll.dll");
+ return {};
+ }
+
+ DWORD major=0, minor=0, build=0;
+ RtlGetNtVersionNumbers(&major, &minor, &build);
+
+ // for whatever reason, the build number has 0xf0000000 set
+ build = 0x0fffffff & build;
+
+ return {major, minor, build};
+}
+
+WindowsInfo::Release WindowsInfo::getRelease() const
+{
+ // there are several interesting items in the registry, but most of them
+ // are undocumented, not always available, and localizable
+ //
+ // most of them are used to provide as much information as possible in case
+ // any of the other versions fail to work
+
+ QSettings settings(
+ R"(HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion)",
+ QSettings::NativeFormat);
+
+ Release r;
+
+ // buildlab seems to be an internal name from the build system
+ r.buildLab = settings.value("BuildLabEx", "").toString();
+ if (r.buildLab.isEmpty()) {
+ r.buildLab = settings.value("BuildLab", "").toString();
+ if (r.buildLab.isEmpty()) {
+ r.buildLab = settings.value("BuildBranch", "").toString();
+ }
+ }
+
+ // localized name of windows, such as "Windows 10 Pro"
+ r.productName = settings.value("ProductName", "").toString();
+
+ // release ID, such as 1803
+ r.ID = settings.value("ReleaseId", "").toString();
+
+ // some other build number, shown in winver.exe
+ r.UBR = settings.value("UBR", 0).toUInt();
+
+ return r;
+}
+
+std::optional<bool> WindowsInfo::getElevated() const
+{
+ HandlePtr token;
+
+ {
+ HANDLE rawToken = 0;
+
+ if (!OpenProcessToken(GetCurrentProcess( ), TOKEN_QUERY, &rawToken)) {
+ const auto e = GetLastError();
+
+ log::error(
+ "while trying to check if process is elevated, "
+ "OpenProcessToken() failed: {}", formatSystemMessage(e));
+
+ return {};
+ }
+
+ token.reset(rawToken);
+ }
+
+ TOKEN_ELEVATION e = {};
+ DWORD size = sizeof(TOKEN_ELEVATION);
+
+ if (!GetTokenInformation(token.get(), TokenElevation, &e, sizeof(e), &size)) {
+ const auto e = GetLastError();
+
+ log::error(
+ "while trying to check if process is elevated, "
+ "GetTokenInformation() failed: {}", formatSystemMessage(e));
+
+ return {};
+ }
+
+ return (e.TokenIsElevated != 0);
+}
+
+} // namespace
diff --git a/src/envwindows.h b/src/envwindows.h
new file mode 100644
index 00000000..90655e49
--- /dev/null
+++ b/src/envwindows.h
@@ -0,0 +1,111 @@
+#ifndef ENV_WINDOWS_H
+#define ENV_WINDOWS_H
+
+#include <QString>
+#include <optional>
+
+namespace env
+{
+
+// a variety of information on windows
+//
+class WindowsInfo
+{
+public:
+ struct Version
+ {
+ DWORD major=0, minor=0, build=0;
+
+ QString toString() const
+ {
+ return QString("%1.%2.%3").arg(major).arg(minor).arg(build);
+ }
+
+ friend bool operator==(const Version& a, const Version& b)
+ {
+ return
+ a.major == b.major &&
+ a.minor == b.minor &&
+ a.build == b.build;
+ }
+
+ friend bool operator!=(const Version& a, const Version& b)
+ {
+ return !(a == b);
+ }
+ };
+
+ struct Release
+ {
+ // the BuildLab entry from the registry, may be empty
+ QString buildLab;
+
+ // product name such as "Windows 10 Pro", may not be in English, may be
+ // empty
+ QString productName;
+
+ // release ID such as 1809, may be mepty
+ QString ID;
+
+ // some sub-build number, undocumented, may be empty
+ DWORD UBR;
+
+ Release()
+ : UBR(0)
+ {
+ }
+ };
+
+
+ WindowsInfo();
+
+ // tries to guess whether this process is running in compatibility mode
+ //
+ bool compatibilityMode() const;
+
+ // returns the Windows version, may not correspond to the actual version
+ // if the process is running in compatibility mode
+ //
+ const Version& reportedVersion() const;
+
+ // tries to guess the real Windows version that's running, can be empty
+ //
+ const Version& realVersion() const;
+
+ // various information about the current release
+ //
+ const Release& release() const;
+
+ // whether this process is running as administrator, may be empty if the
+ // information is not available
+ std::optional<bool> isElevated() const;
+
+ // returns a string with all the above information on one line
+ //
+ QString toString() const;
+
+private:
+ Version m_reported, m_real;
+ Release m_release;
+ std::optional<bool> m_elevated;
+
+ // uses RtlGetVersion() to get the version number as reported by Windows
+ //
+ Version getReportedVersion(HINSTANCE ntdll) const;
+
+ // uses RtlGetNtVersionNumbers() to get the real version number
+ //
+ Version getRealVersion(HINSTANCE ntdll) const;
+
+ // gets various information from the registry
+ //
+ Release getRelease() const;
+
+ // gets whether the process is elevated
+ //
+ std::optional<bool> getElevated() const;
+};
+
+} // namespace
+
+#endif // ENV_WINDOWS_H
diff --git a/src/executableslist.cpp b/src/executableslist.cpp
index 077d2a93..75f29e8f 100644
--- a/src/executableslist.cpp
+++ b/src/executableslist.cpp
@@ -21,6 +21,8 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#include "iplugingame.h"
#include "utility.h"
+#include "settings.h"
+#include <log.h>
#include <QFileInfo>
#include <QDir>
@@ -63,9 +65,9 @@ bool ExecutablesList::empty() const
return m_Executables.empty();
}
-void ExecutablesList::load(const MOBase::IPluginGame* game, QSettings& settings)
+void ExecutablesList::load(const MOBase::IPluginGame* game, const Settings& s)
{
- qDebug("setting up configured executables");
+ log::debug("loading executables");
m_Executables.clear();
@@ -73,58 +75,60 @@ void ExecutablesList::load(const MOBase::IPluginGame* game, QSettings& settings)
// executables from 2.2.0, see upgradeFromCustom()
bool needsUpgrade = false;
- int numCustomExecutables = settings.beginReadArray("customExecutables");
- for (int i = 0; i < numCustomExecutables; ++i) {
- settings.setArrayIndex(i);
-
+ for (auto& map : s.executables()) {
Executable::Flags flags;
- if (settings.value("toolbar", false).toBool())
+
+ if (map["toolbar"].toBool())
flags |= Executable::ShowInToolbar;
- if (settings.value("ownicon", false).toBool())
+
+ if (map["ownicon"].toBool())
flags |= Executable::UseApplicationIcon;
- if (settings.contains("custom")) {
+ if (map["hide"].toBool())
+ flags |= Executable::Hide;
+
+ if (map.contains("custom")) {
// the "custom" setting only exists in older versions
needsUpgrade = true;
}
setExecutable(Executable()
- .title(settings.value("title").toString())
- .binaryInfo(settings.value("binary").toString())
- .arguments(settings.value("arguments").toString())
- .steamAppID(settings.value("steamAppID", "").toString())
- .workingDirectory(settings.value("workingDirectory", "").toString())
+ .title(map["title"].toString())
+ .binaryInfo(map["binary"].toString())
+ .arguments(map["arguments"].toString())
+ .steamAppID(map["steamAppID"].toString())
+ .workingDirectory(map["workingDirectory"].toString())
.flags(flags));
}
- settings.endArray();
-
addFromPlugin(game, IgnoreExisting);
if (needsUpgrade)
upgradeFromCustom(game);
+
+ dump();
}
-void ExecutablesList::store(QSettings& settings)
+void ExecutablesList::store(Settings& s)
{
- settings.remove("customExecutables");
- settings.beginWriteArray("customExecutables");
-
- int count = 0;
+ std::vector<std::map<QString, QVariant>> v;
for (const auto& item : *this) {
- settings.setArrayIndex(count++);
+ std::map<QString, QVariant> map;
+
+ map["title"] = item.title();
+ map["toolbar"] = item.isShownOnToolbar();
+ map["ownicon"] = item.usesOwnIcon();
+ map["hide"] = item.hide();
+ map["binary"] = item.binaryInfo().filePath();
+ map["arguments"] = item.arguments();
+ map["workingDirectory"] = item.workingDirectory();
+ map["steamAppID"] = item.steamAppID();
- settings.setValue("title", item.title());
- settings.setValue("toolbar", item.isShownOnToolbar());
- settings.setValue("ownicon", item.usesOwnIcon());
- settings.setValue("binary", item.binaryInfo().absoluteFilePath());
- settings.setValue("arguments", item.arguments());
- settings.setValue("workingDirectory", item.workingDirectory());
- settings.setValue("steamAppID", item.steamAppID());
+ v.push_back(std::move(map));
}
- settings.endArray();
+ s.setExecutables(v);
}
std::vector<Executable> ExecutablesList::getPluginExecutables(
@@ -163,7 +167,7 @@ std::vector<Executable> ExecutablesList::getPluginExecutables(
void ExecutablesList::resetFromPlugin(MOBase::IPluginGame const *game)
{
- qDebug("resetting plugin executables");
+ log::debug("resetting plugin executables");
Q_ASSERT(game != nullptr);
@@ -240,16 +244,16 @@ void ExecutablesList::setExecutable(const Executable &exe, SetFlags flags)
if (flags == MoveExisting) {
const auto newTitle = makeNonConflictingTitle(exe.title());
if (!newTitle) {
- qCritical().nospace()
- << "executable '" << exe.title() << "' was in the way but could "
- << "not be renamed";
+ log::error(
+ "executable '{}' was in the way but could not be renamed",
+ exe.title());
return;
}
- qWarning().nospace()
- << "executable '" << itor->title() << "' was in the way and was "
- << "renamed to '" << *newTitle << "'";
+ log::warn(
+ "executable '{}' was in the way and was renamed to '{}'",
+ itor->title(), *newTitle);
itor->title(*newTitle);
itor = end();
@@ -286,15 +290,13 @@ std::optional<QString> ExecutablesList::makeNonConflictingTitle(
title = prefix + QString(" (%1)").arg(i);
}
- qCritical().nospace()
- << "ran out of executable titles for prefix '" << prefix << "'";
-
+ log::error("ran out of executable titles for prefix '{}'", prefix);
return {};
}
void ExecutablesList::upgradeFromCustom(MOBase::IPluginGame const *game)
{
- qDebug() << "upgrading executables list";
+ log::debug("upgrading executables list");
Q_ASSERT(game != nullptr);
@@ -332,6 +334,35 @@ void ExecutablesList::upgradeFromCustom(MOBase::IPluginGame const *game)
}
}
+void ExecutablesList::dump() const
+{
+ for (const auto& e : m_Executables) {
+ QStringList flags;
+
+ if (e.flags() & Executable::ShowInToolbar) {
+ flags.push_back("toolbar");
+ }
+
+ if (e.flags() & Executable::UseApplicationIcon) {
+ flags.push_back("icon");
+ }
+
+ if (e.flags() & Executable::Hide) {
+ flags.push_back("hide");
+ }
+
+ log::debug(
+ " . executable '{}'\n"
+ " binary: {}\n"
+ " arguments: {}\n"
+ " steam ID: {}\n"
+ " directory: {}\n"
+ " flags: {} ({})",
+ e.title(), e.binaryInfo().filePath(), e.arguments(),
+ e.steamAppID(), e.workingDirectory(), flags.join("|"), e.flags());
+ }
+}
+
Executable::Executable(QString title)
: m_title(title)
@@ -343,7 +374,7 @@ Executable::Executable(const MOBase::ExecutableInfo& info, Flags flags) :
m_binaryInfo(info.binary()),
m_arguments(info.arguments().join(" ")),
m_steamAppID(info.steamAppID()),
- m_workingDirectory(info.workingDirectory().absolutePath()),
+ m_workingDirectory(info.workingDirectory().path()),
m_flags(flags)
{
}
@@ -433,11 +464,13 @@ bool Executable::usesOwnIcon() const
return m_flags.testFlag(UseApplicationIcon);
}
-void Executable::mergeFrom(const Executable& other)
+bool Executable::hide() const
{
- // flags on plugin executables that the user is allowed to change
- const auto allow = ShowInToolbar;
+ return m_flags.testFlag(Hide);
+}
+void Executable::mergeFrom(const Executable& other)
+{
// this happens after executables are loaded from settings and plugin
// executables are being added, or when users are modifying executables
diff --git a/src/executableslist.h b/src/executableslist.h
index 2d1dd28e..a18042db 100644
--- a/src/executableslist.h
+++ b/src/executableslist.h
@@ -29,6 +29,7 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#include <QMetaType>
namespace MOBase { class IPluginGame; class ExecutableInfo; }
+class Settings;
/*!
* @brief Information about an executable
@@ -38,8 +39,9 @@ class Executable
public:
enum Flag
{
- ShowInToolbar = 0x02,
- UseApplicationIcon = 0x04
+ ShowInToolbar = 0x02,
+ UseApplicationIcon = 0x04,
+ Hide = 0x08
};
Q_DECLARE_FLAGS(Flags, Flag);
@@ -68,6 +70,7 @@ public:
bool isShownOnToolbar() const;
void setShownOnToolbar(bool state);
bool usesOwnIcon() const;
+ bool hide() const;
void mergeFrom(const Executable& other);
@@ -103,7 +106,7 @@ public:
/**
* @brief initializes the list from the settings and the given plugin
**/
- void load(const MOBase::IPluginGame* game, QSettings& settings);
+ void load(const MOBase::IPluginGame* game, const Settings& settings);
/**
* @brief re-adds all the executables from the plugin and renames existing
@@ -114,7 +117,7 @@ public:
/**
* @brief writes the current list to the settings
*/
- void store(QSettings& settings);
+ void store(Settings& settings);
/**
* @brief get an executable by name
@@ -214,6 +217,10 @@ private:
* called when MO is still using the old custom executables from 2.2.0
**/
void upgradeFromCustom(const MOBase::IPluginGame* game);
+
+ // logs all executables
+ //
+ void dump() const;
};
Q_DECLARE_OPERATORS_FOR_FLAGS(Executable::Flags)
diff --git a/src/expanderwidget.cpp b/src/expanderwidget.cpp
deleted file mode 100644
index 2f47da5b..00000000
--- a/src/expanderwidget.cpp
+++ /dev/null
@@ -1,54 +0,0 @@
-#include "expanderwidget.h"
-
-ExpanderWidget::ExpanderWidget()
- : m_button(nullptr), m_content(nullptr), opened_(false)
-{
-}
-
-ExpanderWidget::ExpanderWidget(QToolButton* button, QWidget* content)
- : ExpanderWidget()
-{
- set(button, content);
-}
-
-void ExpanderWidget::set(QToolButton* button, QWidget* content, bool o)
-{
- m_button = button;
- m_content = content;
-
- m_button->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
- QObject::connect(m_button, &QToolButton::clicked, [&]{ toggle(); });
-
- toggle(o);
-}
-
-void ExpanderWidget::toggle()
-{
- if (opened()) {
- toggle(false);
- }
- else {
- toggle(true);
- }
-}
-
-void ExpanderWidget::toggle(bool b)
-{
- if (b) {
- m_button->setArrowType(Qt::DownArrow);
- m_content->show();
- } else {
- m_button->setArrowType(Qt::RightArrow);
- m_content->hide();
- }
-
- // the state has to be remembered instead of using m_content's visibility
- // because saving the state in saveConflictExpandersState() happens after the
- // dialog is closed, which marks all the widgets hidden
- opened_ = b;
-}
-
-bool ExpanderWidget::opened() const
-{
- return opened_;
-}
diff --git a/src/expanderwidget.h b/src/expanderwidget.h
deleted file mode 100644
index da3eb9d6..00000000
--- a/src/expanderwidget.h
+++ /dev/null
@@ -1,46 +0,0 @@
-#ifndef EXPANDERWIDGET_H
-#define EXPANDERWIDGET_H
-
-#include <QToolButton>
-
-/* Takes a QToolButton and a widget and creates an expandable widget.
-**/
-class ExpanderWidget
-{
-public:
- /** empty expander, use set()
- **/
- ExpanderWidget();
-
- /** see set()
- **/
- ExpanderWidget(QToolButton* button, QWidget* content);
-
- /** @brief sets the button and content widgets to use
- * the button will be given an arrow icon, clicking it will toggle the
- * visibility of the given widget
- * @param button the button that toggles the content
- * @param content the widget that will be shown or hidden
- * @param opened initial state, defaults to closed
- **/
- void set(QToolButton* button, QWidget* content, bool opened=false);
-
- /** either opens or closes the expander depending on the current state
- **/
- void toggle();
-
- /** sets the current state of the expander
- **/
- void toggle(bool b);
-
- /** returns whether the expander is currently opened
- **/
- bool opened() const;
-
-private:
- QToolButton* m_button;
- QWidget* m_content;
- bool opened_;
-};
-
-#endif // EXPANDERWIDGET_H
diff --git a/src/filedialogmemory.cpp b/src/filedialogmemory.cpp
index 0e3e9793..8cfeb6b5 100644
--- a/src/filedialogmemory.cpp
+++ b/src/filedialogmemory.cpp
@@ -18,44 +18,21 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
*/
#include "filedialogmemory.h"
+#include "settings.h"
#include <QFileDialog>
+static std::map<QString, QString> g_Cache;
-FileDialogMemory::FileDialogMemory()
+void FileDialogMemory::save(Settings& s)
{
+ s.paths().setRecent(g_Cache);
}
-
-void FileDialogMemory::save(QSettings &settings)
+void FileDialogMemory::restore(const Settings& s)
{
- settings.remove("recentDirectories");
- settings.beginWriteArray("recentDirectories");
- int index = 0;
- for (std::map<QString, QString>::const_iterator iter = instance().m_Cache.begin();
- iter != instance().m_Cache.end(); ++iter) {
- settings.setArrayIndex(index++);
- settings.setValue("name", iter->first);
- settings.setValue("directory", iter->second);
- }
- settings.endArray();
+ g_Cache = s.paths().recent();
}
-
-void FileDialogMemory::restore(QSettings &settings)
-{
- int size = settings.beginReadArray("recentDirectories");
- for (int i = 0; i < size; ++i) {
- settings.setArrayIndex(i);
- QVariant name = settings.value("name");
- QVariant dir = settings.value("directory");
- if (name.isValid() && dir.isValid()) {
- instance().m_Cache.insert(std::make_pair(name.toString(), dir.toString()));
- }
- }
- settings.endArray();
-}
-
-
QString FileDialogMemory::getOpenFileName(
const QString &dirID, QWidget *parent, const QString &caption,
const QString &dir, const QString &filter, QString *selectedFilter,
@@ -64,8 +41,8 @@ QString FileDialogMemory::getOpenFileName(
QString currentDir = dir;
if (currentDir.isEmpty()) {
- auto itor = instance().m_Cache.find(dirID);
- if (itor != instance().m_Cache.end()) {
+ auto itor = g_Cache.find(dirID);
+ if (itor != g_Cache.end()) {
currentDir = itor->second;
}
}
@@ -74,7 +51,7 @@ QString FileDialogMemory::getOpenFileName(
parent, caption, currentDir, filter, selectedFilter, options);
if (!result.isNull()) {
- instance().m_Cache[dirID] = QFileInfo(result).path();
+ g_Cache[dirID] = QFileInfo(result).path();
}
return result;
@@ -88,8 +65,8 @@ QString FileDialogMemory::getExistingDirectory(
QString currentDir = dir;
if (currentDir.isEmpty()) {
- auto itor = instance().m_Cache.find(dirID);
- if (itor != instance().m_Cache.end()) {
+ auto itor = g_Cache.find(dirID);
+ if (itor != g_Cache.end()) {
currentDir = itor->second;
}
}
@@ -98,15 +75,8 @@ QString FileDialogMemory::getExistingDirectory(
parent, caption, currentDir, options);
if (!result.isNull()) {
- instance().m_Cache[dirID] = QFileInfo(result).path();
+ g_Cache[dirID] = result;
}
return result;
}
-
-
-FileDialogMemory &FileDialogMemory::instance()
-{
- static FileDialogMemory instance;
- return instance;
-}
diff --git a/src/filedialogmemory.h b/src/filedialogmemory.h
index 81d7ba40..8b8a3b76 100644
--- a/src/filedialogmemory.h
+++ b/src/filedialogmemory.h
@@ -23,15 +23,17 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#include <map>
#include <QString>
-#include <QSettings>
#include <QFileDialog>
+class Settings;
class FileDialogMemory
{
public:
- static void save(QSettings &settings);
- static void restore(QSettings &settings);
+ FileDialogMemory() = delete;
+
+ static void save(Settings& settings);
+ static void restore(const Settings& settings);
static QString getOpenFileName(
const QString &dirID, QWidget *parent = 0, const QString &caption = QString(),
@@ -42,12 +44,6 @@ public:
const QString &dirID, QWidget *parent = 0, const QString &caption = QString(),
const QString &dir = QString(),
QFileDialog::Options options = QFileDialog::ShowDirsOnly);
-
-private:
- std::map<QString, QString> m_Cache;
-
- FileDialogMemory();
- static FileDialogMemory &instance();
};
#endif // FILEDIALOGMEMORY_H
diff --git a/src/filerenamer.cpp b/src/filerenamer.cpp
index c5c6782b..7fc90eb2 100644
--- a/src/filerenamer.cpp
+++ b/src/filerenamer.cpp
@@ -1,13 +1,17 @@
#include "filerenamer.h"
+#include <utility.h>
+#include <log.h>
#include <QMessageBox>
#include <QFileInfo>
+using namespace MOBase;
+
FileRenamer::FileRenamer(QWidget* parent, QFlags<RenameFlags> flags)
: m_parent(parent), m_flags(flags)
{
// sanity check for flags
if ((m_flags & (HIDE|UNHIDE)) == 0) {
- qCritical("renameFile() missing hide flag");
+ log::error("renameFile() missing hide flag");
// doesn't really matter, it's just for text
m_flags = HIDE;
}
@@ -15,10 +19,10 @@ FileRenamer::FileRenamer(QWidget* parent, QFlags<RenameFlags> flags)
FileRenamer::RenameResults FileRenamer::rename(const QString& oldName, const QString& newName)
{
- qDebug().nospace() << "renaming " << oldName << " to " << newName;
+ log::debug("renaming {} to {}", oldName, newName);
if (QFileInfo(newName).exists()) {
- qDebug().nospace() << newName << " already exists";
+ log::debug("{} already exists", newName);
// target file already exists, confirm replacement
auto answer = confirmReplace(newName);
@@ -26,24 +30,28 @@ FileRenamer::RenameResults FileRenamer::rename(const QString& oldName, const QSt
switch (answer) {
case DECISION_SKIP: {
// user wants to skip this file
- qDebug().nospace() << "skipping " << oldName;
+ log::debug("skipping {}", oldName);
return RESULT_SKIP;
}
case DECISION_REPLACE: {
- qDebug().nospace() << "removing " << newName;
+ log::debug("removing {}", newName);
+
// user wants to replace the file, so remove it
- if (!QFile(newName).remove()) {
- qWarning().nospace() << "failed to remove " << newName;
+ const auto r = shell::Delete(newName);
+
+ if (!r.success()) {
+ log::error("failed to remove '{}': {}", newName, r.toString());
+
// removal failed, warn the user and allow canceling
- if (!removeFailed(newName)) {
- qDebug().nospace() << "canceling " << oldName;
+ if (!removeFailed(newName, r)) {
+ log::debug("canceling {}", oldName);
// user wants to cancel
return RESULT_CANCEL;
}
// ignore this file and continue on
- qDebug().nospace() << "skipping " << oldName;
+ log::debug("skipping {}", oldName);
return RESULT_SKIP;
}
@@ -53,31 +61,34 @@ FileRenamer::RenameResults FileRenamer::rename(const QString& oldName, const QSt
case DECISION_CANCEL: // fall-through
default: {
// user wants to stop
- qDebug().nospace() << "canceling";
+ log::debug("canceling");
return RESULT_CANCEL;
}
}
}
// target either didn't exist or was removed correctly
+ const auto r = shell::Rename(oldName, newName);
- if (!QFile::rename(oldName, newName)) {
- qWarning().nospace() << "failed to rename " << oldName << " to " << newName;
+ if (!r.success()) {
+ log::error(
+ "failed to rename '{}' to '{}': {}",
+ oldName, newName, r.toString());
// renaming failed, warn the user and allow canceling
- if (!renameFailed(oldName, newName)) {
+ if (!renameFailed(oldName, newName, r)) {
// user wants to cancel
- qDebug().nospace() << "canceling";
+ log::debug("canceling");
return RESULT_CANCEL;
}
// ignore this file and continue on
- qDebug().nospace() << "skipping " << oldName;
+ log::debug("skipping {}", oldName);
return RESULT_SKIP;
}
// everything worked
- qDebug().nospace() << "successfully renamed " << oldName << " to " << newName;
+ log::debug("successfully renamed {} to {}", oldName, newName);
return RESULT_OK;
}
@@ -85,12 +96,12 @@ FileRenamer::RenameDecision FileRenamer::confirmReplace(const QString& newName)
{
if (m_flags & REPLACE_ALL) {
// user wants to silently replace all
- qDebug().nospace() << "user has selected replace all";
+ log::debug("user has selected replace all");
return DECISION_REPLACE;
}
else if (m_flags & REPLACE_NONE) {
// user wants to silently skip all
- qDebug().nospace() << "user has selected replace none";
+ log::debug("user has selected replace none");
return DECISION_SKIP;
}
@@ -114,33 +125,33 @@ FileRenamer::RenameDecision FileRenamer::confirmReplace(const QString& newName)
switch (answer) {
case QMessageBox::Yes:
- qDebug().nospace() << "user wants to replace";
+ log::debug("user wants to replace");
return DECISION_REPLACE;
case QMessageBox::No:
- qDebug().nospace() << "user wants to skip";
+ log::debug("user wants to skip");
return DECISION_SKIP;
case QMessageBox::YesToAll:
- qDebug().nospace() << "user wants to replace all";
+ log::debug("user wants to replace all");
// remember the answer
m_flags |= REPLACE_ALL;
return DECISION_REPLACE;
case QMessageBox::NoToAll:
- qDebug().nospace() << "user wants to replace none";
+ log::debug("user wants to replace none");
// remember the answer
m_flags |= REPLACE_NONE;
return DECISION_SKIP;
case QMessageBox::Cancel: // fall-through
default:
- qDebug().nospace() << "user wants to cancel";
+ log::debug("user wants to cancel");
return DECISION_CANCEL;
}
}
-bool FileRenamer::removeFailed(const QString& name)
+bool FileRenamer::removeFailed(const QString& name, const shell::Result& r)
{
QMessageBox::StandardButtons buttons = QMessageBox::Ok;
if (m_flags & MULTIPLE) {
@@ -149,22 +160,24 @@ bool FileRenamer::removeFailed(const QString& name)
}
const auto answer = QMessageBox::critical(
- m_parent, QObject::tr("File operation failed"),
- QObject::tr("Failed to remove \"%1\". Maybe you lack the required file permissions?").arg(name),
+ m_parent,
+ QObject::tr("File operation failed"),
+ QObject::tr("Failed to remove \"%1\": %2").arg(name).arg(r.toString()),
buttons);
if (answer == QMessageBox::Cancel) {
// user wants to stop
- qDebug().nospace() << "user wants to cancel";
+ log::debug("user wants to cancel");
return false;
}
// skip this one and continue
- qDebug().nospace() << "user wants to skip";
+ log::debug("user wants to skip");
return true;
}
-bool FileRenamer::renameFailed(const QString& oldName, const QString& newName)
+bool FileRenamer::renameFailed(
+ const QString& oldName, const QString& newName, const shell::Result& r)
{
QMessageBox::StandardButtons buttons = QMessageBox::Ok;
if (m_flags & MULTIPLE) {
@@ -173,17 +186,24 @@ bool FileRenamer::renameFailed(const QString& oldName, const QString& newName)
}
const auto answer = QMessageBox::critical(
- m_parent, QObject::tr("File operation failed"),
- QObject::tr("failed to rename %1 to %2").arg(oldName).arg(QDir::toNativeSeparators(newName)),
- buttons);
+ m_parent,
+ QObject::tr("File operation failed"),
+ QObject::tr(
+ "Failed to rename file: %1.\r\n\r\n"
+ "Source:\r\n\"%2\"\r\n\r\n"
+ "Destination:\r\n\"%3\"")
+ .arg(r.toString())
+ .arg(QDir::toNativeSeparators(oldName))
+ .arg(QDir::toNativeSeparators(newName)),
+ buttons);
if (answer == QMessageBox::Cancel) {
// user wants to stop
- qDebug().nospace() << "user wants to cancel";
+ log::debug("user wants to cancel");
return false;
}
// skip this one and continue
- qDebug().nospace() << "user wants to skip";
+ log::debug("user wants to skip");
return true;
}
diff --git a/src/filerenamer.h b/src/filerenamer.h
index cd57244c..5583ecbd 100644
--- a/src/filerenamer.h
+++ b/src/filerenamer.h
@@ -3,6 +3,8 @@
#include <QWidget>
+namespace MOBase::shell { class Result; }
+
/**
* Renames individual files and handles dialog boxes to confirm replacements and
* failures with the user
@@ -126,7 +128,7 @@ private:
* @param name The name of the file that failed to be removed
* @return true to continue, false to stop
**/
- bool removeFailed(const QString& name);
+ bool removeFailed(const QString& name, const MOBase::shell::Result& r);
/**
* renaming a file failed, ask the user to continue or cancel
@@ -134,7 +136,9 @@ private:
* @param newName new filename
* @return true to continue, false to stop
**/
- bool renameFailed(const QString& oldName, const QString& newName);
+ bool renameFailed(
+ const QString& oldName, const QString& newName,
+ const MOBase::shell::Result& r);
};
#endif // FILERENAMER_H
diff --git a/src/filterlist.cpp b/src/filterlist.cpp
new file mode 100644
index 00000000..69aca4c5
--- /dev/null
+++ b/src/filterlist.cpp
@@ -0,0 +1,419 @@
+#include "filterlist.h"
+#include "ui_mainwindow.h"
+#include "categories.h"
+#include "categoriesdialog.h"
+#include "settings.h"
+#include <utility.h>
+
+using namespace MOBase;
+using CriteriaType = ModListSortProxy::CriteriaType;
+using Criteria = ModListSortProxy::Criteria;
+
+class FilterList::CriteriaItem : public QTreeWidgetItem
+{
+public:
+ enum States
+ {
+ FirstState = 0,
+
+ Inactive = FirstState,
+ Active,
+ Inverted,
+
+ LastState = Inverted
+ };
+
+ CriteriaItem(FilterList* list, QString name, CriteriaType type, int id)
+ : QTreeWidgetItem({"", name}), m_list(list), m_state(Inactive)
+ {
+ setData(0, Qt::ToolTipRole, name);
+ setData(0, TypeRole, type);
+ setData(0, IDRole, id);
+ }
+
+ CriteriaType type() const
+ {
+ return static_cast<CriteriaType>(data(0, TypeRole).toInt());
+ }
+
+ int id() const
+ {
+ return data(0, IDRole).toInt();
+ }
+
+ States state() const
+ {
+ return m_state;
+ }
+
+ void setState(States s)
+ {
+ if (m_state != s) {
+ m_state = s;
+ updateState();
+ }
+ }
+
+ void nextState()
+ {
+ m_state = static_cast<States>(m_state + 1);
+ if (m_state > LastState) {
+ m_state = FirstState;
+ }
+
+ updateState();
+ }
+
+ void previousState()
+ {
+ m_state = static_cast<States>(m_state - 1);
+ if (m_state < FirstState) {
+ m_state = LastState;
+ }
+
+ updateState();
+ }
+
+private:
+ const int IDRole = Qt::UserRole;
+ const int TypeRole = Qt::UserRole + 1;
+
+ FilterList* m_list;
+ States m_state;
+
+ void updateState()
+ {
+ QString s;
+
+ switch (m_state)
+ {
+ case Inactive:
+ {
+ break;
+ }
+
+ case Active:
+ {
+ // U+2713 CHECK MARK
+ s = QString::fromUtf8("\xe2\x9c\x93");
+ break;
+ }
+
+ case Inverted:
+ {
+ s = tr("Not");
+ break;
+ }
+ }
+
+ setText(0, s);
+ }
+};
+
+
+class CriteriaItemFilter : public QObject
+{
+public:
+ using Callback = std::function<bool (QTreeWidgetItem*, int)>;
+
+ CriteriaItemFilter(QTreeWidget* tree, Callback f)
+ : QObject(tree), m_tree(tree), m_f(std::move(f))
+ {
+ }
+
+ bool eventFilter(QObject* o, QEvent* e) override
+ {
+ // careful: this filter is installed on both the tree and the viewport
+ //
+ // no check is currently necessary because mouse events originate from the
+ // viewport only and keyboard events from the tree only
+
+ if (m_f) {
+ if (e->type() == QEvent::MouseButtonPress || e->type() == QEvent::MouseButtonDblClick) {
+ if (handleMouse(static_cast<QMouseEvent*>(e))) {
+ return true;
+ }
+ } else if (e->type() == QEvent::KeyPress) {
+ if (handleKeyboard(static_cast<QKeyEvent*>(e))) {
+ return true;
+ }
+ }
+ }
+
+ return QObject::eventFilter(o, e);
+ }
+
+private:
+ QTreeWidget* m_tree;
+ Callback m_f;
+
+ bool handleMouse(QMouseEvent* e)
+ {
+ auto* item = m_tree->itemAt(e->pos());
+ if (!item) {
+ return false;
+ }
+
+ m_tree->setCurrentItem(item);
+
+ const auto dir = (e->button() == Qt::LeftButton ? 1 : - 1);
+
+ return m_f(item, dir);
+ }
+
+ bool handleKeyboard(QKeyEvent* e)
+ {
+ if (e->key() == Qt::Key_Space) {
+ auto* item = m_tree->currentItem();
+ if (!item) {
+ return false;
+ }
+
+ const auto shiftPressed = (e->modifiers() & Qt::ShiftModifier);
+ const auto dir = (shiftPressed ? -1 : 1);
+
+ return m_f(item, dir);
+ }
+
+ return false;
+ }
+};
+
+
+FilterList::FilterList(Ui::MainWindow* ui, CategoryFactory& factory)
+ : ui(ui), m_factory(factory)
+{
+ auto* eventFilter = new CriteriaItemFilter(
+ ui->filters, [&](auto* item, int dir){ return cycleItem(item, dir); });
+
+ ui->filters->installEventFilter(eventFilter);
+ ui->filters->viewport()->installEventFilter(eventFilter);
+
+ connect(ui->filtersClear, &QPushButton::clicked, [&]{ clearSelection(); });
+ connect(ui->filtersEdit, &QPushButton::clicked, [&]{ editCategories(); });
+ connect(ui->filtersAnd, &QCheckBox::toggled, [&]{ onOptionsChanged(); });
+ connect(ui->filtersOr, &QCheckBox::toggled, [&]{ onOptionsChanged(); });
+
+ connect(
+ ui->filtersSeparators, qOverload<int>(&QComboBox::currentIndexChanged),
+ [&]{ onOptionsChanged(); });
+
+ ui->filters->header()->setMinimumSectionSize(0);
+ ui->filters->header()->setSectionResizeMode(0, QHeaderView::Fixed);
+ ui->filters->header()->resizeSection(0, 30);
+ ui->categoriesSplitter->setCollapsible(0, false);
+ ui->categoriesSplitter->setCollapsible(1, false);
+
+ ui->filtersSeparators->addItem(tr("Filter separators"), ModListSortProxy::SeparatorFilter);
+ ui->filtersSeparators->addItem(tr("Show separators"), ModListSortProxy::SeparatorShow);
+ ui->filtersSeparators->addItem(tr("Hide separators"), ModListSortProxy::SeparatorHide);
+}
+
+void FilterList::restoreState(const Settings& s)
+{
+ s.widgets().restoreIndex(ui->filtersSeparators);
+}
+
+void FilterList::saveState(Settings& s) const
+{
+ s.widgets().saveIndex(ui->filtersSeparators);
+}
+
+QTreeWidgetItem* FilterList::addCriteriaItem(
+ QTreeWidgetItem *root, const QString &name, int categoryID,
+ CriteriaType type)
+{
+ auto* item = new CriteriaItem(this, name, type, categoryID);
+
+ if (root != nullptr) {
+ root->addChild(item);
+ } else {
+ ui->filters->addTopLevelItem(item);
+ }
+
+ item->setTextAlignment(0, Qt::AlignCenter);
+
+ return item;
+}
+
+void FilterList::addContentCriteria()
+{
+ for (unsigned i = 0; i < ModInfo::NUM_CONTENT_TYPES; ++i) {
+ QString filterName = tr("Contains %1").arg(ModInfo::getContentTypeName(i));
+ addCriteriaItem(
+ nullptr, QString("<%1>").arg(filterName),
+ i, ModListSortProxy::TypeContent);
+ }
+}
+
+void FilterList::addCategoryCriteria(QTreeWidgetItem *root, const std::set<int> &categoriesUsed, int targetID)
+{
+ const auto count = static_cast<unsigned int>(m_factory.numCategories());
+ for (unsigned int i = 1; i < count; ++i) {
+ if (m_factory.getParentID(i) == targetID) {
+ int categoryID = m_factory.getCategoryID(i);
+ if (categoriesUsed.find(categoryID) != categoriesUsed.end()) {
+ QTreeWidgetItem *item =
+ addCriteriaItem(root, m_factory.getCategoryName(i),
+ categoryID, ModListSortProxy::TypeCategory);
+ if (m_factory.hasChildren(i)) {
+ addCategoryCriteria(item, categoriesUsed, categoryID);
+ }
+ }
+ }
+ }
+}
+
+void FilterList::addSpecialCriteria(int type)
+{
+ const auto sc = static_cast<CategoryFactory::SpecialCategories>(type);
+
+ addCriteriaItem(
+ nullptr, m_factory.getSpecialCategoryName(sc),
+ type, ModListSortProxy::TypeSpecial);
+}
+
+void FilterList::refresh()
+{
+ const auto oldSelection = selectedCriteria();
+
+ ui->filters->clear();
+
+ using F = CategoryFactory;
+ addSpecialCriteria(F::Checked);
+ addSpecialCriteria(F::UpdateAvailable);
+ addSpecialCriteria(F::Backup);
+ addSpecialCriteria(F::Managed);
+ addSpecialCriteria(F::HasCategory);
+ addSpecialCriteria(F::Conflict);
+ addSpecialCriteria(F::Endorsed);
+ addSpecialCriteria(F::Tracked);
+ addSpecialCriteria(F::HasNexusID);
+ addSpecialCriteria(F::HasGameData);
+
+ addContentCriteria();
+
+ std::set<int> categoriesUsed;
+ for (unsigned int modIdx = 0; modIdx < ModInfo::getNumMods(); ++modIdx) {
+ ModInfo::Ptr modInfo = ModInfo::getByIndex(modIdx);
+ for (int categoryID : modInfo->getCategories()) {
+ int currentID = categoryID;
+ std::set<int> cycleTest;
+ // also add parents so they show up in the tree
+ while (currentID != 0) {
+ categoriesUsed.insert(currentID);
+ if (!cycleTest.insert(currentID).second) {
+ log::warn("cycle in categories: {}", SetJoin(cycleTest, ", "));
+ break;
+ }
+ currentID = m_factory.getParentID(m_factory.getCategoryIndex(currentID));
+ }
+ }
+ }
+
+ addCategoryCriteria(nullptr, categoriesUsed, 0);
+ setSelection(oldSelection);
+}
+
+void FilterList::setSelection(const std::vector<Criteria>& criteria)
+{
+ for (int i = 0; i < ui->filters->topLevelItemCount(); ++i) {
+ auto* item = dynamic_cast<CriteriaItem*>(ui->filters->topLevelItem(i));
+ if (!item) {
+ continue;
+ }
+
+ bool found = false;
+
+ for (auto&& c : criteria) {
+ if (item->type() == c.type && item->id() == c.id) {
+ item->setState(c.inverse ? CriteriaItem::Inverted : CriteriaItem::Active);
+ found = true;
+ break;
+ }
+ }
+
+ if (!found) {
+ item->setState(CriteriaItem::Inactive);
+ }
+ }
+}
+
+void FilterList::clearSelection()
+{
+ for (int i=0; i<ui->filters->topLevelItemCount(); ++i) {
+ auto* ci = dynamic_cast<CriteriaItem*>(ui->filters->topLevelItem(i));
+ if (!ci) {
+ continue;
+ }
+
+ ci->setState(CriteriaItem::Inactive);
+ }
+
+ checkCriteria();
+}
+
+bool FilterList::cycleItem(QTreeWidgetItem* item, int direction)
+{
+ auto* ci = dynamic_cast<CriteriaItem*>(item);
+ if (!ci) {
+ return false;
+ }
+
+ if (direction > 0) {
+ ci->nextState();
+ } else if (direction < 0) {
+ ci->previousState();
+ } else {
+ return false;
+ }
+
+ checkCriteria();
+ return true;
+}
+
+std::vector<ModListSortProxy::Criteria> FilterList::selectedCriteria() const
+{
+ std::vector<Criteria> criteria;
+
+ for (int i=0; i<ui->filters->topLevelItemCount(); ++i) {
+ const auto* ci = dynamic_cast<CriteriaItem*>(ui->filters->topLevelItem(i));
+ if (!ci) {
+ continue;
+ }
+
+ if (ci->state() != CriteriaItem::Inactive) {
+ criteria.push_back({
+ ci->type(), ci->id(), (ci->state() == CriteriaItem::Inverted)
+ });
+ }
+ }
+
+ return criteria;
+}
+
+void FilterList::checkCriteria()
+{
+ emit criteriaChanged(selectedCriteria());
+}
+
+void FilterList::editCategories()
+{
+ CategoriesDialog dialog(qApp->activeWindow());
+
+ if (dialog.exec() == QDialog::Accepted) {
+ dialog.commitChanges();
+ refresh();
+ }
+}
+
+void FilterList::onOptionsChanged()
+{
+ const auto mode = ui->filtersAnd->isChecked() ?
+ ModListSortProxy::FilterAnd: ModListSortProxy::FilterOr;
+
+ const auto separators = static_cast<ModListSortProxy::SeparatorsMode>(
+ ui->filtersSeparators->currentData().toInt());
+
+ emit optionsChanged(mode, separators);
+}
diff --git a/src/filterlist.h b/src/filterlist.h
new file mode 100644
index 00000000..b0ebc9a4
--- /dev/null
+++ b/src/filterlist.h
@@ -0,0 +1,55 @@
+#ifndef MODORGANIZER_CATEGORIESLIST_INCLUDED
+#define MODORGANIZER_CATEGORIESLIST_INCLUDED
+
+#include "modlistsortproxy.h"
+#include <QTreeWidgetItem>
+
+namespace Ui { class MainWindow; };
+class CategoryFactory;
+class Settings;
+
+class FilterList : public QObject
+{
+ Q_OBJECT;
+
+public:
+ FilterList(Ui::MainWindow* ui, CategoryFactory& factory);
+
+ void restoreState(const Settings& s);
+ void saveState(Settings& s) const;
+
+ void setSelection(const std::vector<ModListSortProxy::Criteria>& criteria);
+ void clearSelection();
+ void refresh();
+
+signals:
+ void criteriaChanged(std::vector<ModListSortProxy::Criteria> criteria);
+ void optionsChanged(
+ ModListSortProxy::FilterMode mode, ModListSortProxy::SeparatorsMode sep);
+
+private:
+ class CriteriaItem;
+
+ Ui::MainWindow* ui;
+ CategoryFactory& m_factory;
+
+ bool onClick(QMouseEvent* e);
+ void onItemActivated(QTreeWidgetItem* item);
+ void onOptionsChanged();
+
+ void editCategories();
+ void checkCriteria();
+ std::vector<ModListSortProxy::Criteria> selectedCriteria() const;
+ bool cycleItem(QTreeWidgetItem* item, int direction);
+
+ QTreeWidgetItem* addCriteriaItem(
+ QTreeWidgetItem *root, const QString &name, int categoryID,
+ ModListSortProxy::CriteriaType type);
+
+ void addContentCriteria();
+ void addCategoryCriteria(
+ QTreeWidgetItem *root, const std::set<int> &categoriesUsed, int targetID);
+ void addSpecialCriteria(int type);
+};
+
+#endif // MODORGANIZER_CATEGORIESLIST_INCLUDED
diff --git a/src/filterwidget.cpp b/src/filterwidget.cpp
index 44cbb274..0638add3 100644
--- a/src/filterwidget.cpp
+++ b/src/filterwidget.cpp
@@ -1,5 +1,8 @@
#include "filterwidget.h"
#include "eventfilter.h"
+#include <log.h>
+
+using namespace MOBase;
FilterWidgetProxyModel::FilterWidgetProxyModel(FilterWidget& fw, QWidget* parent)
: QSortFilterProxyModel(parent), m_filter(fw)
@@ -80,7 +83,7 @@ QModelIndex FilterWidget::map(const QModelIndex& index)
if (m_proxy) {
return m_proxy->mapToSource(index);
} else {
- qCritical() << "FilterWidget::map() called, but proxy isn't set up";
+ log::error("FilterWidget::map() called, but proxy isn't set up");
return index;
}
}
diff --git a/src/forcedloaddialogwidget.cpp b/src/forcedloaddialogwidget.cpp
index b92838c3..b84f785f 100644
--- a/src/forcedloaddialogwidget.cpp
+++ b/src/forcedloaddialogwidget.cpp
@@ -1,9 +1,8 @@
#include "forcedloaddialogwidget.h"
#include "ui_forcedloaddialogwidget.h"
-
-#include <QFileDialog>
-
#include "executableinfo.h"
+#include <log.h>
+#include <QFileDialog>
using namespace MOBase;
@@ -85,7 +84,7 @@ void ForcedLoadDialogWidget::on_libraryPathBrowseButton_clicked()
if (fileInfo.exists()) {
ui->libraryPathEdit->setText(filePath);
} else {
- qCritical("%ls does not exist", filePath.toStdWString().c_str());
+ log::error("{} does not exist", filePath);
}
}
}
@@ -102,7 +101,7 @@ void ForcedLoadDialogWidget::on_processBrowseButton_clicked()
if (fileInfo.exists()) {
ui->processEdit->setText(fileName);
} else {
- qCritical("%ls does not exist", fileInfo.filePath().toStdWString().c_str());
+ log::error("{} does not exist", fileInfo.filePath());
}
}
}
diff --git a/src/helper.cpp b/src/helper.cpp
deleted file mode 100644
index 59a2d3d1..00000000
--- a/src/helper.cpp
+++ /dev/null
@@ -1,125 +0,0 @@
-/*
-Copyright (C) 2012 Sebastian Herbord. All rights reserved.
-
-This file is part of Mod Organizer.
-
-Mod Organizer is free software: you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation, either version 3 of the License, or
-(at your option) any later version.
-
-Mod Organizer is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
-*/
-
-#include "helper.h"
-#include "utility.h"
-#include <report.h>
-#include <LMCons.h>
-
-#define WIN32_LEAN_AND_MEAN
-#include <Windows.h>
-
-#include <QDir>
-#include <QApplication>
-
-using MOBase::reportError;
-
-
-namespace Helper {
-
-
-static bool helperExec(LPCWSTR moDirectory, LPCWSTR commandLine, BOOL async)
-{
- wchar_t fileName[MAX_PATH];
- _snwprintf(fileName, MAX_PATH, L"%ls\\helper.exe", moDirectory);
-
- SHELLEXECUTEINFOW execInfo = {0};
-
- execInfo.cbSize = sizeof(SHELLEXECUTEINFOW);
- execInfo.fMask = SEE_MASK_NOCLOSEPROCESS;
- execInfo.hwnd = nullptr;
- execInfo.lpVerb = L"runas";
- execInfo.lpFile = fileName;
- execInfo.lpParameters = commandLine;
- execInfo.lpDirectory = moDirectory;
- execInfo.nShow = SW_SHOW;
-
- ::ShellExecuteExW(&execInfo);
-
- if (execInfo.hProcess == 0) {
- reportError(QObject::tr("helper failed"));
- return false;
- }
-
- if (async) {
- return true;
- }
-
- if (::WaitForSingleObject(execInfo.hProcess, INFINITE) != WAIT_OBJECT_0) {
- reportError(QObject::tr("helper failed"));
- return false;
- }
-
- DWORD exitCode;
- GetExitCodeProcess(execInfo.hProcess, &exitCode);
- return exitCode == NOERROR;
-}
-
-
-bool init(const std::wstring &moPath, const std::wstring &dataPath)
-{
- DWORD userNameLen = UNLEN + 1;
- wchar_t userName[UNLEN + 1];
-
- if (!GetUserName(userName, &userNameLen)) {
- reportError(QObject::tr("failed to determine account name"));
- return false;
- }
- wchar_t *commandLine = new wchar_t[32768];
-
- _snwprintf(commandLine, 32768, L"init \"%ls\" \"%ls\"",
- dataPath.c_str(), userName);
-
- bool res = helperExec(moPath.c_str(), commandLine, FALSE);
- delete [] commandLine;
-
- return res;
-}
-
-
-bool backdateBSAs(const std::wstring &moPath, const std::wstring &dataPath)
-{
- wchar_t *commandLine = new wchar_t[32768];
- _snwprintf(commandLine, 32768, L"backdateBSA \"%ls\"",
- dataPath.c_str());
-
- bool res = helperExec(moPath.c_str(), commandLine, FALSE);
- delete [] commandLine;
-
- return res;
-}
-
-
-bool adminLaunch(const std::wstring &moPath, const std::wstring &moFile, const std::wstring &workingDir)
-{
- wchar_t *commandLine = new wchar_t[32768];
- _snwprintf(commandLine, 32768, L"adminLaunch %d \"%ls\" \"%ls\"",
- ::GetCurrentProcessId(),
- moFile.c_str(),
- workingDir.c_str()
- );
-
- bool res = helperExec(moPath.c_str(), commandLine, TRUE);
- delete [] commandLine;
-
- return res;
-}
-
-
-} // namespace
diff --git a/src/helper.h b/src/helper.h
deleted file mode 100644
index f6667a84..00000000
--- a/src/helper.h
+++ /dev/null
@@ -1,64 +0,0 @@
-/*
-Copyright (C) 2012 Sebastian Herbord. All rights reserved.
-
-This file is part of Mod Organizer.
-
-Mod Organizer is free software: you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation, either version 3 of the License, or
-(at your option) any later version.
-
-Mod Organizer is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
-*/
-
-#ifndef HELPER_H
-#define HELPER_H
-
-
-#include <string>
-
-
-/**
- * @brief Convenience functions to work with the external helper program.
- *
- * The mo_helper program is used to make changes on the system that require administrative
- * rights, so that ModOrganizer itself can run without special privileges
- **/
-namespace Helper {
-
-/**
- * @brief initialise the specified directory for use with mod organizer.
- *
- * This will create all required sub-directories and give the user running ModOrganizer
- * write-access
- *
- * @param moPath absolute path to the ModOrganizer base directory
- * @return true on success
- **/
-bool init(const std::wstring &moPath, const std::wstring &dataPath);
-
-/**
- * @brief sets the last modified time for all .bsa-files in the target directory well into the past
- * @param moPath absolute path to the modOrganizer base directory
- * @param dataPath the path taht contains the .bsa-files, usually the data directory of the game
- **/
-bool backdateBSAs(const std::wstring &moPath, const std::wstring &dataPath);
-
-/**
- * @brief waits for the current process to exit and restarts it as an administrator
- * @param moPath absolute path to the modOrganizer base directory
- * @param moFile file name of modOrganizer
- * @param workingDir current working directory
- **/
-bool adminLaunch(const std::wstring &moPath, const std::wstring &moFile, const std::wstring &workingDir);
-
-}
-
-
-#endif // HELPER_H
diff --git a/src/icondelegate.cpp b/src/icondelegate.cpp
index 249dae6f..03964263 100644
--- a/src/icondelegate.cpp
+++ b/src/icondelegate.cpp
@@ -18,25 +18,24 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
*/
#include "icondelegate.h"
+#include <log.h>
#include <QHBoxLayout>
#include <QLabel>
#include <QPainter>
#include <QDebug>
#include <QPixmapCache>
+using namespace MOBase;
IconDelegate::IconDelegate(QObject *parent)
: QStyledItemDelegate(parent)
{
}
-
-void IconDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
+void IconDelegate::paintIcons(
+ QPainter *painter, const QStyleOptionViewItem &option,
+ const QModelIndex &index, const QList<QString>& icons)
{
- QStyledItemDelegate::paint(painter, option, index);
-
- QList<QString> icons = getIcons(index);
-
int x = 4;
painter->save();
@@ -54,7 +53,7 @@ void IconDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option,
if (!QPixmapCache::find(fullIconId, &icon)) {
icon = QIcon(iconId).pixmap(iconWidth, iconWidth);
if (icon.isNull()) {
- qWarning("failed to load icon %s", qUtf8Printable(iconId));
+ log::warn("failed to load icon {}", iconId);
}
QPixmapCache::insert(fullIconId, icon);
}
@@ -65,3 +64,9 @@ void IconDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option,
painter->restore();
}
+void IconDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
+{
+ QStyledItemDelegate::paint(painter, option, index);
+ paintIcons(painter, option, index, getIcons(index));
+}
+
diff --git a/src/icondelegate.h b/src/icondelegate.h
index 39694481..bac71a62 100644
--- a/src/icondelegate.h
+++ b/src/icondelegate.h
@@ -27,25 +27,19 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
class IconDelegate : public QStyledItemDelegate
{
- Q_OBJECT
-public:
+ Q_OBJECT;
+public:
explicit IconDelegate(QObject *parent = 0);
-
virtual void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const;
-signals:
-
-public slots:
-
-private:
+ static void paintIcons(
+ QPainter *painter, const QStyleOptionViewItem &option,
+ const QModelIndex &index, const QList<QString>& icons);
+protected:
virtual QList<QString> getIcons(const QModelIndex &index) const = 0;
virtual size_t getNumIcons(const QModelIndex &index) const = 0;
-
-
-private:
-
};
#endif // ICONDELEGATE_H
diff --git a/src/ilockedwaitingforprocess.h b/src/ilockedwaitingforprocess.h
deleted file mode 100644
index 9475ddb9..00000000
--- a/src/ilockedwaitingforprocess.h
+++ /dev/null
@@ -1,13 +0,0 @@
-#ifndef ILOCKEDWAITINGFORPROCESS_H
-#define ILOCKEDWAITINGFORPROCESS_H
-
-class QString;
-
-class ILockedWaitingForProcess
-{
-public:
- virtual bool unlockForced() const = 0;
- virtual void setProcessName(QString const &) = 0;
-};
-
-#endif // ILOCKEDWAITINGFORPROCESS_H
diff --git a/src/installationmanager.cpp b/src/installationmanager.cpp
index e443a8f2..c3aeec88 100644
--- a/src/installationmanager.cpp
+++ b/src/installationmanager.cpp
@@ -263,7 +263,7 @@ QStringList InstallationManager::extractFiles(const QStringList &filesOrig, bool
targetFile = wcsrchr(origFile/*data[i]->getFileName()*/, '/');
}
if (targetFile == nullptr) {
- qCritical() << "Failed to find backslash in " << data[i]->getFileName();
+ log::error("Failed to find backslash in {}", data[i]->getFileName());
continue;
} else {
// skip the slash
@@ -307,7 +307,16 @@ QStringList InstallationManager::extractFiles(const QStringList &filesOrig, bool
QCoreApplication::processEvents();
} while (!future.isFinished() || m_InstallationProgress->isVisible());
if (!future.result()) {
- throw MyException(QString("extracting failed (%1)").arg(m_ArchiveHandler->getLastError()));
+ if (m_ArchiveHandler->getLastError() == Archive::ERROR_EXTRACT_CANCELLED) {
+ if (!m_ErrorMessage.isEmpty()) {
+ throw MyException(tr("Extraction failed: %1").arg(m_ErrorMessage));
+ } else {
+ return QStringList();
+ }
+ }
+ else {
+ throw MyException(tr("Extraction failed: %1").arg(m_ArchiveHandler->getLastError()));
+ }
}
return result;
@@ -319,14 +328,9 @@ IPluginInstaller::EInstallResult InstallationManager::installArchive(GuessedValu
// a problem if this is called by the bundle installer and the bundled installer adds additional names that then end up being used,
// because the caller will then not have the right name.
bool iniTweaks;
- if (install(archiveName, modName, iniTweaks, modId)) {
- return IPluginInstaller::RESULT_SUCCESS;
- } else {
- return IPluginInstaller::RESULT_FAILED;
- }
+ return install(archiveName, modName, iniTweaks, modId);
}
-
DirectoryTree *InstallationManager::createFilesTree()
{
FileData* const *data;
@@ -398,7 +402,7 @@ bool InstallationManager::isSimpleArchiveTopLayer(const DirectoryTree::Node *nod
for (DirectoryTree::const_node_iterator iter = node->nodesBegin(); iter != node->nodesEnd(); ++iter) {
if ((bainStyle && InstallationTester::isTopLevelDirectoryBain((*iter)->getData().name)) ||
(!bainStyle && InstallationTester::isTopLevelDirectory((*iter)->getData().name))) {
- qDebug("%s on the top level", (*iter)->getData().name.toUtf8().constData());
+ log::debug("{} on the top level", (*iter)->getData().name.toQString());
return true;
}
}
@@ -424,7 +428,7 @@ DirectoryTree::Node *InstallationManager::getSimpleArchiveBase(DirectoryTree *da
(currentNode->numNodes() == 1)) {
currentNode = *currentNode->nodesBegin();
} else {
- qDebug("not a simple archive");
+ log::debug("not a simple archive");
return nullptr;
}
}
@@ -476,17 +480,22 @@ QString InstallationManager::generateBackupName(const QString &directoryName) co
bool InstallationManager::testOverwrite(GuessedValue<QString> &modName, bool *merge) const
{
QString targetDirectory = QDir::fromNativeSeparators(m_ModsDirectory + "\\" + modName);
+
while (QDir(targetDirectory).exists()) {
Settings &settings(Settings::instance());
- bool backup = settings.directInterface().value("backup_install", false).toBool();
- QueryOverwriteDialog overwriteDialog(m_ParentWidget,
- backup ? QueryOverwriteDialog::BACKUP_YES : QueryOverwriteDialog::BACKUP_NO);
+
+ const bool backup = settings.keepBackupOnInstall();
+ QueryOverwriteDialog overwriteDialog(
+ m_ParentWidget,
+ backup ? QueryOverwriteDialog::BACKUP_YES : QueryOverwriteDialog::BACKUP_NO);
+
if (overwriteDialog.exec()) {
- settings.directInterface().setValue("backup_install", overwriteDialog.backup());
+ settings.setKeepBackupOnInstall(overwriteDialog.backup());
+
if (overwriteDialog.backup()) {
QString backupDirectory = generateBackupName(targetDirectory);
if (!copyDir(targetDirectory, backupDirectory, false)) {
- reportError(tr("failed to create backup"));
+ reportError(tr("Failed to create backup"));
return false;
}
}
@@ -527,7 +536,7 @@ bool InstallationManager::testOverwrite(GuessedValue<QString> &modName, bool *me
settingsFile.write(originalSettings);
settingsFile.close();
} else {
- qCritical("failed to restore original settings: %s", qUtf8Printable(metaFilename));
+ log::error("failed to restore original settings: {}", metaFilename);
}
return true;
} else if (overwriteDialog.action() == QueryOverwriteDialog::ACT_MERGE) {
@@ -559,24 +568,24 @@ bool InstallationManager::ensureValidModName(GuessedValue<QString> &name) const
return true;
}
-bool InstallationManager::doInstall(GuessedValue<QString> &modName, QString gameName, int modID,
+IPluginInstaller::EInstallResult InstallationManager::doInstall(GuessedValue<QString> &modName, QString gameName, int modID,
const QString &version, const QString &newestVersion,
int categoryID, int fileCategoryID, const QString &repository)
{
if (!ensureValidModName(modName)) {
- return false;
+ return IPluginInstaller::RESULT_FAILED;
}
bool merge = false;
// determine target directory
if (!testOverwrite(modName, &merge)) {
- return false;
+ return IPluginInstaller::RESULT_FAILED;
}
QString targetDirectory = QDir(m_ModsDirectory + "/" + modName).canonicalPath();
QString targetDirectoryNative = QDir::toNativeSeparators(targetDirectory);
- qDebug("installing to \"%s\"", qUtf8Printable(targetDirectoryNative));
+ log::debug("installing to \"{}\"", targetDirectoryNative);
m_InstallationProgress = new QProgressDialog(m_ParentWidget);
ON_BLOCK_EXIT([this] () {
@@ -611,12 +620,12 @@ bool InstallationManager::doInstall(GuessedValue<QString> &modName, QString game
if (!future.result()) {
if (m_ArchiveHandler->getLastError() == Archive::ERROR_EXTRACT_CANCELLED) {
if (!m_ErrorMessage.isEmpty()) {
- throw MyException(QString("extracting failed (%1)").arg(m_ErrorMessage));
+ throw MyException(tr("Extraction failed: %1").arg(m_ErrorMessage));
} else {
- return false;
+ return IPluginInstaller::RESULT_CANCELED;
}
} else {
- throw MyException(QString("extracting failed (%1)").arg(m_ArchiveHandler->getLastError()));
+ throw MyException(tr("Extraction failed: %1").arg(m_ArchiveHandler->getLastError()));
}
}
@@ -657,7 +666,7 @@ bool InstallationManager::doInstall(GuessedValue<QString> &modName, QString game
settingsFile.endGroup();
}
- return true;
+ return IPluginInstaller::RESULT_SUCCESS;
}
@@ -693,7 +702,7 @@ void InstallationManager::postInstallCleanup()
QFile::setPermissions(fileInfo.absoluteFilePath(), QFile::ReadOther | QFile::WriteOther);
}
if (!QFile::remove(fileInfo.absoluteFilePath())) {
- qWarning() << "Unable to delete " << fileInfo.absoluteFilePath();
+ log::warn("Unable to delete {}", fileInfo.absoluteFilePath());
}
}
directoriesToRemove.insert(fileInfo.absolutePath());
@@ -707,7 +716,7 @@ void InstallationManager::postInstallCleanup()
}
}
-bool InstallationManager::install(const QString &fileName,
+IPluginInstaller::EInstallResult InstallationManager::install(const QString &fileName,
GuessedValue<QString> &modName,
bool &hasIniTweaks,
int modID)
@@ -718,7 +727,7 @@ bool InstallationManager::install(const QString &fileName,
QFileInfo fileInfo(fileName);
if (m_SupportedExtensions.find(fileInfo.suffix()) == m_SupportedExtensions.end()) {
reportError(tr("File format \"%1\" not supported").arg(fileInfo.suffix()));
- return false;
+ return IPluginInstaller::RESULT_FAILED;
}
modName.setFilter(&fixDirectoryName);
@@ -764,7 +773,7 @@ bool InstallationManager::install(const QString &fileName,
if ((modID == 0) && (guessedModID != -1)) {
modID = guessedModID;
} else if (modID != guessedModID) {
- qDebug("passed mod id: %d, guessed id: %d", modID, guessedModID);
+ log::debug("passed mod id: {}, guessed id: {}", modID, guessedModID);
}
modName.update(guessedModName, GUESS_GOOD);
@@ -774,7 +783,7 @@ bool InstallationManager::install(const QString &fileName,
if (fileInfo.dir() == QDir(m_DownloadsDirectory)) {
m_CurrentFile = fileInfo.fileName();
}
- qDebug("using mod name \"%s\" (id %d) -> %s", qUtf8Printable(modName), modID, qUtf8Printable(m_CurrentFile));
+ log::debug("using mod name \"{}\" (id {}) -> {}", QString(modName), modID, m_CurrentFile);
//If there's an archive already open, close it. This happens with the bundle
//installer when it uncompresses a split archive, then finds it has a real archive
@@ -785,9 +794,9 @@ bool InstallationManager::install(const QString &fileName,
bool archiveOpen = m_ArchiveHandler->open(fileName,
new MethodCallback<InstallationManager, void, QString *>(this, &InstallationManager::queryPassword));
if (!archiveOpen) {
- qDebug("integrated archiver can't open %s: %s (%d)",
- qUtf8Printable(fileName),
- qUtf8Printable(getErrorString(m_ArchiveHandler->getLastError())),
+ log::debug("integrated archiver can't open {}: {} ({})",
+ fileName,
+ getErrorString(m_ArchiveHandler->getLastError()),
m_ArchiveHandler->getLastError());
}
ON_BLOCK_EXIT(std::bind(&InstallationManager::postInstallCleanup, this));
@@ -826,15 +835,12 @@ bool InstallationManager::install(const QString &fileName,
mapToArchive(filesTree.data());
// the simple installer only prepares the installation, the rest
// works the same for all installers
- if (!doInstall(modName, gameName, modID, version, newestVersion, categoryID,
- fileCategoryID, repository)) {
- installResult = IPluginInstaller::RESULT_FAILED;
- }
+ installResult = doInstall(modName, gameName, modID, version, newestVersion, categoryID, fileCategoryID, repository);
}
}
}
- { // custom case
+ if (installResult != IPluginInstaller::RESULT_CANCELED) { // custom case
IPluginInstallerCustom *installerCustom
= dynamic_cast<IPluginInstallerCustom *>(installer);
if ((installerCustom != nullptr)
@@ -856,35 +862,41 @@ bool InstallationManager::install(const QString &fileName,
}
}
} catch (const IncompatibilityException &e) {
- qCritical("plugin \"%s\" incompatible: %s",
- qUtf8Printable(installer->name()), e.what());
+ log::error("plugin \"{}\" incompatible: {}", installer->name(), e.what());
}
// act upon the installation result. at this point the files have already been
// extracted to the correct location
switch (installResult) {
- case IPluginInstaller::RESULT_CANCELED:
case IPluginInstaller::RESULT_FAILED: {
- return false;
+ QMessageBox::information(qApp->activeWindow(), tr("Installation failed"),
+ tr("Something went wrong while installing this mod."),
+ QMessageBox::Ok);
+ return installResult;
} break;
case IPluginInstaller::RESULT_SUCCESS:
case IPluginInstaller::RESULT_SUCCESSCANCEL: {
if (filesTree != nullptr) {
DirectoryTree::node_iterator iniTweakNode = filesTree->nodeFind(DirectoryTreeInformation("INI Tweaks"));
hasIniTweaks = (iniTweakNode != filesTree->nodesEnd()) &&
- ((*iniTweakNode)->numLeafs() != 0);
- return true;
- } else {
- return false;
+ ((*iniTweakNode)->numLeafs() != 0);
}
+ return IPluginInstaller::RESULT_SUCCESS;
} break;
+ case IPluginInstaller::RESULT_NOTATTEMPTED:
+ case IPluginInstaller::RESULT_MANUALREQUESTED: {
+ continue;
+ }
+ default:
+ return installResult;
}
}
+ if (installResult == IPluginInstaller::RESULT_NOTATTEMPTED) {
+ reportError(tr("None of the available installer plugins were able to handle that archive.\n"
+ "This is likely due to a corrupted or incompatible download or unrecognized archive format."));
+ }
- reportError(tr("None of the available installer plugins were able to handle that archive.\n"
- "This is likely due to a corrupted or incompatible download or unrecognized archive format."));
-
- return false;
+ return installResult;
}
diff --git a/src/installationmanager.h b/src/installationmanager.h
index 6e4f5925..199a0f82 100644
--- a/src/installationmanager.h
+++ b/src/installationmanager.h
@@ -82,7 +82,7 @@ public:
* @return true if the archive was installed, false if installation failed or was refused
* @exception std::exception an exception may be thrown if the archive can't be opened (maybe the format is invalid or the file is damaged)
**/
- bool install(const QString &fileName, MOBase::GuessedValue<QString> &modName, bool &hasIniTweaks, int modID = 0);
+ MOBase::IPluginInstaller::EInstallResult install(const QString &fileName, MOBase::GuessedValue<QString> &modName, bool &hasIniTweaks, int modID = 0);
/**
* @return true if the installation was canceled
@@ -175,7 +175,7 @@ private:
bool isSimpleArchiveTopLayer(const MOBase::DirectoryTree::Node *node, bool bainStyle);
MOBase::DirectoryTree::Node *getSimpleArchiveBase(MOBase::DirectoryTree *dataTree);
- bool doInstall(MOBase::GuessedValue<QString> &modName, QString gameName,
+ MOBase::IPluginInstaller::EInstallResult doInstall(MOBase::GuessedValue<QString> &modName, QString gameName,
int modID, const QString &version, const QString &newestVersion, int categoryID, int fileCategoryID, const QString &repository);
//QString generateBackupName(const QString &directoryName) const;
diff --git a/src/instancemanager.cpp b/src/instancemanager.cpp
index ddc2d067..a9da30d9 100644
--- a/src/instancemanager.cpp
+++ b/src/instancemanager.cpp
@@ -21,6 +21,7 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#include "instancemanager.h"
#include "selectiondialog.h"
#include <utility.h>
+#include <log.h>
#include <appconfig.h>
#include <QCoreApplication>
#include <QDir>
@@ -29,13 +30,13 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#include <QMessageBox>
#include <cstdint>
+using namespace MOBase;
static const char COMPANY_NAME[] = "Tannin";
static const char APPLICATION_NAME[] = "Mod Organizer";
static const char INSTANCE_KEY[] = "CurrentInstance";
-
InstanceManager::InstanceManager()
: m_AppSettings(COMPANY_NAME, APPLICATION_NAME)
{
@@ -86,10 +87,13 @@ bool InstanceManager::deleteLocalInstance(const QString &instanceId) const
if (!MOBase::shellDelete(QStringList(instancePath),true))
{
- qWarning("Failed to shell-delete \"%s\" (errorcode %lu), trying regular delete", qUtf8Printable(instancePath), ::GetLastError());
+ log::warn(
+ "Failed to shell-delete \"{}\" (errorcode {}), trying regular delete",
+ instancePath, ::GetLastError());
+
if (!MOBase::removeDir(instancePath))
{
- qWarning("regular delete failed too");
+ log::warn("regular delete failed too");
result = false;
}
}
@@ -153,7 +157,7 @@ QString InstanceManager::queryInstanceName(const QStringList &instanceList) cons
dialogText = dialog.textValue();
instanceId = sanitizeInstanceName(dialogText);
if (instanceId != dialogText) {
- if (QMessageBox::question( nullptr,
+ if (QMessageBox::question( nullptr,
QObject::tr("Invalid instance name"),
QObject::tr("The instance name \"%1\" is invalid. Use the name \"%2\" instead?").arg(dialogText,instanceId),
QMessageBox::Yes | QMessageBox::No) == QMessageBox::No) {
@@ -182,6 +186,10 @@ QString InstanceManager::queryInstanceName(const QStringList &instanceList) cons
QString InstanceManager::chooseInstance(const QStringList &instanceList) const
{
+ if (portableInstallIsLocked()) {
+ return QString();
+ }
+
enum class Special : uint8_t {
NewInstance,
Portable,
@@ -220,7 +228,7 @@ QString InstanceManager::chooseInstance(const QStringList &instanceList) const
selection.setWindowFlags(selection.windowFlags() | Qt::WindowStaysOnTopHint);
if (selection.exec() == QDialog::Rejected) {
- qDebug("rejected");
+ log::debug("rejected");
throw MOBase::MyException(QObject::tr("Canceled"));
}
@@ -251,10 +259,30 @@ QString InstanceManager::instancePath() const
QStringList InstanceManager::instances() const
{
- return QDir(instancePath()).entryList(QDir::Dirs | QDir::NoDotAndDotDot);
+ const std::set<QString> ignore = {
+ "cache", "qtwebengine",
+ };
+
+ const auto dirs = QDir(instancePath())
+ .entryList(QDir::Dirs | QDir::NoDotAndDotDot);
+
+ QStringList list;
+
+ for (auto&& d : dirs) {
+ if (!ignore.contains(QFileInfo(d).fileName().toLower())) {
+ list.push_back(d);
+ }
+ }
+
+ return list;
}
+bool InstanceManager::isPortablePath(const QString& dataPath)
+{
+ return (dataPath == qApp->applicationDirPath());
+}
+
bool InstanceManager::portableInstall() const
{
return QFile::exists(qApp->applicationDirPath() + "/" +
@@ -262,6 +290,19 @@ bool InstanceManager::portableInstall() const
}
+bool InstanceManager::portableInstallIsLocked() const
+{
+ return QFile::exists(qApp->applicationDirPath() + "/" +
+ QString::fromStdWString(AppConfig::portableLockFileName()));
+}
+
+
+bool InstanceManager::allowedToChangeInstance() const
+{
+ return !portableInstallIsLocked();
+}
+
+
void InstanceManager::createDataPath(const QString &dataPath) const
{
if (!QDir(dataPath).exists()) {
@@ -282,6 +323,10 @@ void InstanceManager::createDataPath(const QString &dataPath) const
QString InstanceManager::determineDataPath()
{
QString instanceId = currentInstance();
+ if (portableInstallIsLocked())
+ {
+ instanceId.clear();
+ }
if (instanceId.isEmpty() && !m_Reset && (m_overrideInstance || portableInstall()))
{
// startup, apparently using portable mode before
@@ -323,7 +368,7 @@ QString InstanceManager::sanitizeInstanceName(const QString &name) const
// Don't end in spaces and periods
new_name = new_name.remove(QRegExp("\\.*$"));
new_name = new_name.remove(QRegExp(" *$"));
-
+
// Recurse until stuff stops changing
if (new_name != name) {
return sanitizeInstanceName(new_name);
diff --git a/src/instancemanager.h b/src/instancemanager.h
index 4efa6f03..649c2195 100644
--- a/src/instancemanager.h
+++ b/src/instancemanager.h
@@ -38,6 +38,9 @@ public:
QString currentInstance() const;
+ bool allowedToChangeInstance() const;
+ static bool isPortablePath(const QString& dataPath);
+
private:
InstanceManager();
@@ -58,6 +61,7 @@ private:
void createDataPath(const QString &dataPath) const;
bool portableInstall() const;
+ bool portableInstallIsLocked() const;
private:
diff --git a/src/iuserinterface.h b/src/iuserinterface.h
index bba8de2b..aa48194f 100644
--- a/src/iuserinterface.h
+++ b/src/iuserinterface.h
@@ -3,21 +3,15 @@
#include "modinfodialogfwd.h"
-#include "ilockedwaitingforprocess.h"
#include <iplugintool.h>
#include <ipluginmodpage.h>
#include <delayedfilewriter.h>
-
#include <QMenu>
-class QSettings;
class IUserInterface
{
public:
-
- virtual void storeSettings(QSettings &settings) = 0;
-
virtual void registerPluginTool(MOBase::IPluginTool *tool, QString name = QString(), QMenu *menu = nullptr) = 0;
virtual void registerPluginTools(std::vector<MOBase::IPluginTool *> toolPlugins) = 0;
virtual void registerModPage(MOBase::IPluginModPage *modPage) = 0;
@@ -36,8 +30,7 @@ public:
virtual MOBase::DelayedFileWriterBase &archivesWriter() = 0;
- virtual ILockedWaitingForProcess* lock() = 0;
- virtual void unlock() = 0;
+ virtual QWidget* qtWidget() = 0;
};
#endif // IUSERINTERFACE_H
diff --git a/src/json.h b/src/json.h
new file mode 100644
index 00000000..d182f330
--- /dev/null
+++ b/src/json.h
@@ -0,0 +1,190 @@
+#ifndef MODORGANIZER_JSON_INCLUDED
+#define MODORGANIZER_JSON_INCLUDED
+
+#include <log.h>
+#include <QJsonDocument>
+#include <QJsonObject>
+#include <QJsonArray>
+#include <QJsonValue>
+
+namespace json
+{
+
+class failed {};
+
+
+namespace details
+{
+
+QString typeName(const QJsonValue& v)
+{
+ if (v.isUndefined()) {
+ return "undefined";
+ } else if (v.isNull()) {
+ return "null";
+ } else if (v.isArray()) {
+ return "an array";
+ } else if (v.isBool()) {
+ return "a bool";
+ } else if (v.isDouble()) {
+ return "a double";
+ } else if (v.isObject()) {
+ return "an object";
+ } else if (v.isString()) {
+ return "a string";
+ } else {
+ return "an unknown type";
+ }
+}
+
+QString typeName(const QJsonDocument& doc)
+{
+ if (doc.isEmpty()) {
+ return "empty";
+ } else if (doc.isNull()) {
+ return "null";
+ } else if (doc.isArray()) {
+ return "an array";
+ } else if (doc.isObject()) {
+ return "an object";
+ } else {
+ return "an unknown type";
+ }
+}
+
+
+template <class T>
+T convert(const QJsonValue& v) = delete;
+
+template <>
+bool convert<bool>(const QJsonValue& v)
+{
+ if (!v.isBool()) {
+ throw failed();
+ }
+
+ return v.toBool();
+}
+
+template <>
+QJsonObject convert<QJsonObject>(const QJsonValue& v)
+{
+ if (!v.isObject()) {
+ throw failed();
+ }
+
+ return v.toObject();
+}
+
+template <>
+QString convert<QString>(const QJsonValue& v)
+{
+ if (!v.isString()) {
+ throw failed();
+ }
+
+ return v.toString();
+}
+
+template <>
+QJsonArray convert<QJsonArray>(const QJsonValue& v)
+{
+ if (!v.isArray()) {
+ throw failed();
+ }
+
+ return v.toArray();
+}
+
+template <>
+qint64 convert<qint64>(const QJsonValue& v)
+{
+ if (!v.isDouble()) {
+ throw failed();
+ }
+
+ return static_cast<qint64>(v.toDouble());
+}
+
+} // namespace
+
+
+template <class T>
+T convert(const QJsonValue& value, const char* what)
+{
+ try
+ {
+ return details::convert<T>(value);
+ }
+ catch(failed&)
+ {
+ MOBase::log::error(
+ "'{}' is a {}, not a {}",
+ what, details::typeName(value), typeid(T).name);
+
+ throw;
+ }
+}
+
+template <class T>
+T convertWarn(const QJsonValue& value, const char* what, T def={})
+{
+ try
+ {
+ return details::convert<T>(value);
+ }
+ catch(failed&)
+ {
+ MOBase::log::warn(
+ "'{}' is a {}, not a {}",
+ what, details::typeName(value), typeid(T).name());
+
+ return def;
+ }
+}
+
+template <class T>
+T get(const QJsonObject& o, const char* e)
+{
+ if (!o.contains(e)) {
+ MOBase::log::error("property '{}' is missing", e);
+ throw failed();
+ }
+
+ return convert<T>(o[e], e);
+}
+
+template <class T>
+T getWarn(const QJsonObject& o, const char* e, T def={})
+{
+ if (!o.contains(e)) {
+ MOBase::log::warn("property '{}' is missing", e);
+ return def;
+ }
+
+ return convertWarn<T>(o[e], e);
+}
+
+template <class T>
+T getOpt(const QJsonObject& o, const char* e, T def={})
+{
+ if (!o.contains(e)) {
+ return def;
+ }
+
+ return convertWarn<T>(o[e], e);
+}
+
+
+template <class Value>
+void requireObject(const Value& v, const char* what)
+{
+ if (!v.isObject()) {
+ MOBase::log::error("{} is {}, not an object", what, details::typeName(v));
+ throw failed();
+ }
+}
+
+} // namespace
+
+#endif // MODORGANIZER_JSON_INCLUDED
diff --git a/src/listdialog.cpp b/src/listdialog.cpp
index 52bba80d..2ad88408 100644
--- a/src/listdialog.cpp
+++ b/src/listdialog.cpp
@@ -17,6 +17,7 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#include "listdialog.h"
#include "ui_listdialog.h"
+#include "settings.h"
ListDialog::ListDialog(QWidget *parent)
: QDialog(parent)
@@ -32,6 +33,12 @@ ListDialog::~ListDialog()
delete ui;
}
+int ListDialog::exec()
+{
+ GeometrySaver gs(Settings::instance(), this);
+ return QDialog::exec();
+}
+
void ListDialog::setChoices(QStringList choices)
{
m_Choices = choices;
@@ -61,7 +68,7 @@ void ListDialog::on_filterEdit_textChanged(QString filter)
if (newChoices.length() == 1) {
QListWidgetItem *item = ui->choiceList->item(0);
- ui->choiceList->setItemSelected(item, true);
+ item->setSelected(true);
ui->choiceList->setCurrentItem(item);
}
diff --git a/src/listdialog.h b/src/listdialog.h
index 7b5a5461..d0594bd7 100644
--- a/src/listdialog.h
+++ b/src/listdialog.h
@@ -15,6 +15,10 @@ public:
explicit ListDialog(QWidget *parent = nullptr);
~ListDialog();
+ // also saves and restores geometry
+ //
+ int exec() override;
+
void setChoices(QStringList choices);
QString getChoice() const;
diff --git a/src/loadmechanism.cpp b/src/loadmechanism.cpp
index 8f0529ce..0c81b7b2 100644
--- a/src/loadmechanism.cpp
+++ b/src/loadmechanism.cpp
@@ -23,6 +23,7 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#include <iplugingame.h>
#include <scriptextender.h>
#include <appconfig.h>
+#include <log.h>
#include <QFile>
#include <QFileInfo>
#include <QDir>
@@ -31,286 +32,33 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#include <QCryptographicHash>
#include <QCoreApplication>
-
using namespace MOBase;
using namespace MOShared;
-
LoadMechanism::LoadMechanism()
: m_SelectedMechanism(LOAD_MODORGANIZER)
{
}
-void LoadMechanism::writeHintFile(const QDir &targetDirectory)
-{
- QString hintFilePath = targetDirectory.absoluteFilePath("mo_path.txt");
- QFile hintFile(hintFilePath);
- if (hintFile.exists()) {
- hintFile.remove();
- }
- if (!hintFile.open(QIODevice::WriteOnly)) {
- throw MyException(QObject::tr("failed to open %1: %2").arg(hintFilePath).arg(hintFile.errorString()));
- }
- hintFile.write(qApp->applicationDirPath().toUtf8().constData());
- hintFile.close();
-}
-
-
-void LoadMechanism::removeHintFile(QDir targetDirectory)
-{
- targetDirectory.remove("mo_path.txt");
-}
-
-
-bool LoadMechanism::isDirectLoadingSupported()
-{
- //FIXME: Seriously? isn't there a 'do i need steam' thing?
- IPluginGame const *game = qApp->property("managed_game").value<IPluginGame*>();
- if (game->gameName().compare("oblivion", Qt::CaseInsensitive) == 0) {
- // oblivion can be loaded directly if it's not the steam variant
- return !game->gameDirectory().exists("steam_api.dll");
- } else {
- // all other games work afaik
- return true;
- }
-}
-
-bool LoadMechanism::isScriptExtenderSupported()
-{
- IPluginGame const *game = qApp->property("managed_game").value<IPluginGame*>();
- ScriptExtender *extender = game->feature<ScriptExtender>();
-
- // test if there even is an extender for the managed game and if so whether it's installed
- return extender != nullptr && extender->isInstalled();
-}
-
-bool LoadMechanism::isProxyDLLSupported()
+bool LoadMechanism::isDirectLoadingSupported() const
{
- // using steam_api.dll as the proxy is way too game specific as many games will have different
- // versions of that dll.
- // plus: the proxy dll hasn't been working for at least the whole 1.12.x versions of MO and
- // noone reported it so why maintain an unused feature?
- return false;
-/* IPluginGame const *game = qApp->property("managed_game").value<IPluginGame*>();
- return game->gameDirectory().exists(QString::fromStdWString(AppConfig::proxyDLLTarget()));*/
+ return true;
}
-
-bool LoadMechanism::hashIdentical(const QString &fileNameLHS, const QString &fileNameRHS)
+void LoadMechanism::activate(EMechanism)
{
- QFile fileLHS(fileNameLHS);
- if (!fileLHS.open(QIODevice::ReadOnly)) {
- throw MyException(QObject::tr("file not found: %1").arg(qUtf8Printable(fileNameLHS)));
- }
- QByteArray dataLHS = fileLHS.readAll();
- QByteArray hashLHS = QCryptographicHash::hash(dataLHS, QCryptographicHash::Md5);
-
- fileLHS.close();
-
- QFile fileRHS(fileNameRHS);
- if (!fileRHS.open(QIODevice::ReadOnly)) {
- throw MyException(QObject::tr("file not found: %1").arg(qUtf8Printable(fileNameRHS)));
- }
- QByteArray dataRHS = fileRHS.readAll();
- QByteArray hashRHS = QCryptographicHash::hash(dataRHS, QCryptographicHash::Md5);
-
- fileRHS.close();
-
- return hashLHS == hashRHS;
+ // no-op
}
-void LoadMechanism::deactivateScriptExtender()
+QString toString(LoadMechanism::EMechanism e)
{
- try {
- IPluginGame const *game = qApp->property("managed_game").value<IPluginGame*>();
- ScriptExtender *extender = game->feature<ScriptExtender>();
- if (extender == nullptr) {
- return;
- }
-
- QDir pluginsDir(game->gameDirectory().absolutePath() + "/data/" + extender->PluginPath());
-
-#pragma message("implement this for usvfs")
-
- QString vfsDLLName = "";
- if (extender->getArch() == IMAGE_FILE_MACHINE_I386) {
- vfsDLLName = ToQString(AppConfig::vfs32DLLName());
- }
- else if (extender->getArch() == IMAGE_FILE_MACHINE_AMD64)
- {
- vfsDLLName = ToQString(AppConfig::vfs64DLLName());
- }
- qDebug("USVFS DLL Name: " + vfsDLLName.toLatin1());
- if (vfsDLLName != "") {
- if (QFile(pluginsDir.absoluteFilePath(vfsDLLName)).exists()) {
- // remove dll from SE plugins directory
- if (!pluginsDir.remove(vfsDLLName)) {
- throw MyException(QObject::tr("Failed to delete %1").arg(pluginsDir.absoluteFilePath(vfsDLLName)));
- }
- }
- }
+ switch (e)
+ {
+ case LoadMechanism::LOAD_MODORGANIZER:
+ return "ModOrganizer";
- removeHintFile(pluginsDir);
- } catch (const std::exception &e) {
- QMessageBox::critical(nullptr, QObject::tr("Failed to deactivate script extender loading"), e.what());
+ default:
+ return QString("unknown (%1)").arg(static_cast<int>(e));
}
}
-
-
-void LoadMechanism::deactivateProxyDLL()
-{
- try {
- IPluginGame const *game = qApp->property("managed_game").value<IPluginGame *>();
-
- QString targetPath = game->gameDirectory().absoluteFilePath(QString::fromStdWString(AppConfig::proxyDLLTarget()));
-
- QFile targetDLL(targetPath);
- if (targetDLL.exists()) {
- QString origFile = game->gameDirectory().absoluteFilePath(QString::fromStdWString(AppConfig::proxyDLLOrig()));
- // determine if a proxy-dll is installed
- // this is a very crude way of making this decision but it should be good enough
- if ((targetDLL.size() < 24576) && (QFile(origFile).exists())) {
- // remove proxy-dll
- if (!targetDLL.remove()) {
- throw MyException(QObject::tr("Failed to remove %1: %2").arg(targetPath).arg(targetDLL.errorString()));
- } else if (!QFile::rename(origFile, targetPath)) {
- throw MyException(QObject::tr("Failed to rename %1 to %2").arg(origFile, targetPath));
- }
- }
- }
-
- removeHintFile(game->gameDirectory());
- } catch (const std::exception &e) {
- QMessageBox::critical(nullptr, QObject::tr("Failed to deactivate proxy-dll loading"), e.what());
- }
-}
-
-
-void LoadMechanism::activateScriptExtender()
-{
- try {
- IPluginGame const *game = qApp->property("managed_game").value<IPluginGame *>();
- ScriptExtender *extender = game->feature<ScriptExtender>();
- if (extender == nullptr) {
- return;
- }
-
- QDir pluginsDir(game->gameDirectory().absolutePath() + "/data/" + extender->PluginPath());
-
- if (!pluginsDir.exists()) {
- pluginsDir.mkpath(".");
- }
-
-#pragma message("implement this for usvfs")
- std::wstring vfsDLL = L"";
- if (extender->getArch() == IMAGE_FILE_MACHINE_I386) {
- vfsDLL = AppConfig::vfs32DLLName();
- }
- else if (extender->getArch() == IMAGE_FILE_MACHINE_AMD64)
- {
- vfsDLL = AppConfig::vfs64DLLName();
- }
- if (vfsDLL != L"") {
- QString targetPath = pluginsDir.absoluteFilePath(ToQString(vfsDLL));
- QString vfsDLLPath = qApp->applicationDirPath() + "/" + QString::fromStdWString(vfsDLL);
-
- qDebug("DLL USVFS Target Path: " + targetPath.toLatin1());
- qDebug("DLL USVFS VFS DLL Path: " + vfsDLLPath.toLatin1());
-
- QFile dllFile(targetPath);
-
- if (dllFile.exists()) {
- // may be outdated
- if (!hashIdentical(targetPath, vfsDLLPath)) {
- dllFile.remove();
- }
- }
-
- if (!dllFile.exists()) {
- // install dll to SE plugins
- if (!QFile::copy(vfsDLLPath, targetPath)) {
- throw MyException(QObject::tr("Failed to copy %1 to %2").arg(vfsDLLPath, targetPath));
- }
- }
- }
- writeHintFile(pluginsDir);
- } catch (const std::exception &e) {
- QMessageBox::critical(nullptr, QObject::tr("Failed to set up script extender loading"), e.what());
- }
-}
-
-
-void LoadMechanism::activateProxyDLL()
-{
- try {
- IPluginGame const *game = qApp->property("managed_game").value<IPluginGame *>();
-
- QString targetPath = game->gameDirectory().absoluteFilePath(QString::fromStdWString(AppConfig::proxyDLLTarget()));
-
- QFile targetDLL(targetPath);
- if (!targetDLL.exists()) {
- return;
- }
-
- QString sourcePath = qApp->applicationDirPath() + "/" + ToQString(AppConfig::proxyDLLSource());
-
- // this is a very crude way of making this decision but it should be good enough
- if (targetDLL.size() < 24576) {
- // determine if a proxy-dll is already installed and if so, if it's the right one
- if (!hashIdentical(targetPath, sourcePath)) {
- // wrong proxy dll, probably outdated. delete and install the new one
- if (!QFile::remove(targetPath)) {
- throw MyException(QObject::tr("Failed to delete old proxy-dll %1").arg(targetPath));
- }
- if (!QFile::copy(sourcePath, targetPath)) {
- throw MyException(QObject::tr("Failed to copy %1 to %2").arg(sourcePath).arg(targetPath));
- }
- } // otherwise the proxy-dll is already the right one
- } else {
- // no proxy dll installed yet. move the original and insert proxy-dll
-
- QString origFile = game->gameDirectory().absoluteFilePath(QString::fromStdWString(AppConfig::proxyDLLOrig()));
-
- if (QFile(origFile).exists()) {
- // orig-file exists. this may happen if the steam-api was updated or the user messed with the
- // dlls.
- if (!QFile::remove(origFile)) {
- throw MyException(QObject::tr("Failed to overwrite %1").arg(origFile));
- }
- }
- if (!QFile::rename(targetPath, origFile)) {
- throw MyException(QObject::tr("Failed to rename %1 to %2").arg(targetPath).arg(origFile));
- }
- if (!QFile::copy(sourcePath, targetPath)) {
- throw MyException(QObject::tr("Failed to copy %1 to %2").arg(sourcePath).arg(targetPath));
- }
- }
- writeHintFile(game->gameDirectory());
- } catch (const std::exception &e) {
- QMessageBox::critical(nullptr, QObject::tr("Failed to set up proxy-dll loading"), e.what());
- }
-}
-
-
-void LoadMechanism::activate(EMechanism mechanism)
-{
- switch (mechanism) {
- case LOAD_MODORGANIZER: {
- qDebug("Load Mechanism: Mod Organizer");
- deactivateProxyDLL();
- deactivateScriptExtender();
- } break;
- case LOAD_SCRIPTEXTENDER: {
- qDebug("Load Mechanism: ScriptExtender");
- deactivateProxyDLL();
- activateScriptExtender();
- } break;
- case LOAD_PROXYDLL: {
- qDebug("Load Mechanism: Proxy DLL");
- deactivateScriptExtender();
- activateProxyDLL();
- } break;
- }
-}
-
diff --git a/src/loadmechanism.h b/src/loadmechanism.h
index c04473ab..151e804f 100644
--- a/src/loadmechanism.h
+++ b/src/loadmechanism.h
@@ -27,33 +27,14 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
/**
* @brief manages the various load mechanisms supported by Mod Organizer
- * the load mechanisms is the means by which the mo-dll is injected into the target
- * process. The default mode "mod organizer" requires the target process to be started
- * from inside mod organizer. In certain cases (oblivion steam edition) this is not
- * possible since the game can then only be started from steam.
- * "Script Extender" is an alternative load mechanism that uses a script extender (obse,
- * fose, nvse or skse) to load MO. This is reliable but prevents se plugins installed
- * through MO from working.
- * "Proxy DLL" replaces a dll belonging to the game by a proxy that will load MO and then
- * chain-load the original dll. This currently only works with steam-versions of games and
- * is intended as a last resort solution.
**/
class LoadMechanism
{
public:
-
enum EMechanism {
LOAD_MODORGANIZER = 0,
- LOAD_SCRIPTEXTENDER,
- LOAD_PROXYDLL
};
-public:
-
- /**
- * @brief constructor
- *
- **/
LoadMechanism();
/**
@@ -66,55 +47,15 @@ public:
/**
* @brief test whether the "Mod Organizer" load mechanism is supported for the current game
*
- * @return true if the load mechanism is supported
- **/
- bool isDirectLoadingSupported();
-
- /**
- * @brief test whether the "Script Extender" load mechanism is supported for the current game
- *
- * @return true if the load mechanism is supported
- **/
- bool isScriptExtenderSupported();
-
- /**
- * @brief test whether the "Proxy DLL" load mechanism is supported for the current game
- *
- * @return true if the load mechanism is supported
+ * @return true
**/
- bool isProxyDLLSupported();
+ bool isDirectLoadingSupported() const;
private:
-
- // write a hint file that is required for certain loading mechanisms for the dll to find
- // the mod organizer installation
- void writeHintFile(const QDir &targetDirectory);
-
- // remove the hint file if it exists. does nothing if the file doesn't exist
- void removeHintFile(QDir targetDirectory);
-
- // compare the two files by md5-hash, returns true if they are identical
- bool hashIdentical(const QString &fileNameLHS, const QString &fileNameRHS);
-
- // deactivate loading through script extender. does nothing if se-loading wasn't active
- void deactivateScriptExtender();
-
- // deactivate loading through proxy-dll. does nothing if se-loading wasn't active
- void deactivateProxyDLL();
-
- // activate loading through script extender. does nothing if already active. updates
- // the dll if necessary
- void activateScriptExtender();
-
- // activate loading through proxy-dll. does nothing if already active. updates
- // the dll if necessary
- void activateProxyDLL();
-
-private:
-
EMechanism m_SelectedMechanism;
-
};
+QString toString(LoadMechanism::EMechanism e);
+
#endif // LOADMECHANISM_H
diff --git a/src/lockeddialog.cpp b/src/lockeddialog.cpp
deleted file mode 100644
index 143d5838..00000000
--- a/src/lockeddialog.cpp
+++ /dev/null
@@ -1,71 +0,0 @@
-/*
-Copyright (C) 2012 Sebastian Herbord. All rights reserved.
-
-This file is part of Mod Organizer.
-
-Mod Organizer is free software: you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation, either version 3 of the License, or
-(at your option) any later version.
-
-Mod Organizer is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
-*/
-
-#include "lockeddialog.h"
-#include "ui_lockeddialog.h"
-
-#include <QPoint>
-#include <QResizeEvent>
-#include <QWidget>
-#include <Qt> // for Qt::FramelessWindowHint, etc
-
-LockedDialog::LockedDialog(QWidget *parent, bool unlockByButton)
- : LockedDialogBase(parent, !unlockByButton)
- , ui(new Ui::LockedDialog)
-{
- ui->setupUi(this);
-
- // Supposedly the Qt::CustomizeWindowHint should use a customized window
- // allowing us to select if there is a close button. In practice this doesn't
- // seem to work. We will ignore pressing the close button if unlockByButton == true
- Qt::WindowFlags flags =
- this->windowFlags() | Qt::CustomizeWindowHint | Qt::WindowTitleHint | Qt::WindowMinimizeButtonHint;
- if (m_allowClose)
- flags |= Qt::WindowCloseButtonHint;
- this->setWindowFlags(flags);
-
- if (!unlockByButton)
- {
- ui->unlockButton->hide();
- ui->verticalLayout->addItem(
- new QSpacerItem(20, 40, QSizePolicy::Minimum, QSizePolicy::Expanding));
- }
-}
-
-LockedDialog::~LockedDialog()
-{
- delete ui;
-}
-
-
-void LockedDialog::setProcessName(const QString &name)
-{
- ui->processLabel->setText(name);
-}
-
-void LockedDialog::on_unlockButton_clicked()
-{
- unlock();
-}
-
-void LockedDialog::unlock() {
- LockedDialogBase::unlock();
- ui->label->setText("unlocking may take a few seconds");
- ui->unlockButton->setEnabled(false);
-}
diff --git a/src/lockeddialog.h b/src/lockeddialog.h
deleted file mode 100644
index 36c16429..00000000
--- a/src/lockeddialog.h
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
-Copyright (C) 2012 Sebastian Herbord. All rights reserved.
-
-This file is part of Mod Organizer.
-
-Mod Organizer is free software: you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation, either version 3 of the License, or
-(at your option) any later version.
-
-Mod Organizer is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
-*/
-
-#pragma once
-
-#include "lockeddialogbase.h"
-
-namespace Ui {
- class LockedDialog;
-}
-
-/**
- * a small borderless dialog displayed while the Mod Organizer UI is locked
- * The dialog contains only a label and a button to force the UI to be unlocked
- *
- * The UI gets locked while running external applications since they may modify the
- * data on which Mod Organizer works. After the UI is unlocked (manually or after the
- * external application closed) MO will refresh all of its data sources
- **/
-class LockedDialog : public LockedDialogBase
-{
- Q_OBJECT
-
-public:
- explicit LockedDialog(QWidget *parent = 0, bool unlockByButton = false);
- ~LockedDialog();
-
- void setProcessName(const QString &name) override;
-
-protected:
-
- void unlock() override;
-
-private slots:
-
- void on_unlockButton_clicked();
-
-private:
-
- Ui::LockedDialog *ui;
-};
diff --git a/src/lockeddialog.ui b/src/lockeddialog.ui
deleted file mode 100644
index 0ec2e467..00000000
--- a/src/lockeddialog.ui
+++ /dev/null
@@ -1,98 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<ui version="4.0">
- <class>LockedDialog</class>
- <widget class="QDialog" name="LockedDialog">
- <property name="geometry">
- <rect>
- <x>0</x>
- <y>0</y>
- <width>317</width>
- <height>151</height>
- </rect>
- </property>
- <property name="windowTitle">
- <string>Running virtualized processes</string>
- </property>
- <layout class="QVBoxLayout" name="verticalLayout" stretch="1,0,0">
- <item>
- <widget class="QLabel" name="label">
- <property name="toolTip">
- <string>This dialog should disappear automatically if the application/game is done. Click unlock if it didn't.</string>
- </property>
- <property name="text">
- <string>MO is locked while the executable is running.</string>
- </property>
- <property name="alignment">
- <set>Qt::AlignCenter</set>
- </property>
- <property name="wordWrap">
- <bool>true</bool>
- </property>
- </widget>
- </item>
- <item>
- <spacer name="verticalSpacer1">
- <property name="orientation">
- <enum>Qt::Vertical</enum>
- </property>
- <property name="sizeHint" stdset="0">
- <size>
- <height>10</height>
- </size>
- </property>
- </spacer>
- </item>
- <item>
- <widget class="QLabel" name="processLabel">
- <property name="font">
- <font>
- <italic>true</italic>
- </font>
- </property>
- <property name="styleSheet">
- <string notr="true">color: grey;</string>
- </property>
- <property name="text">
- <string/>
- </property>
- <property name="alignment">
- <set>Qt::AlignCenter</set>
- </property>
- </widget>
- </item>
- <item>
- <spacer name="verticalSpacer2">
- <property name="orientation">
- <enum>Qt::Vertical</enum>
- </property>
- <property name="sizeHint" stdset="0">
- <size>
- <height>10</height>
- </size>
- </property>
- </spacer>
- </item>
- <item>
- <widget class="QPushButton" name="unlockButton">
- <property name="text">
- <string>Unlock</string>
- </property>
- </widget>
- </item>
- <item>
- <spacer name="verticalSpacer3">
- <property name="orientation">
- <enum>Qt::Vertical</enum>
- </property>
- <property name="sizeHint" stdset="0">
- <size>
- <height>10</height>
- </size>
- </property>
- </spacer>
- </item>
- </layout>
- </widget>
- <resources/>
- <connections/>
-</ui>
diff --git a/src/lockeddialogbase.cpp b/src/lockeddialogbase.cpp
deleted file mode 100644
index b18f7429..00000000
--- a/src/lockeddialogbase.cpp
+++ /dev/null
@@ -1,73 +0,0 @@
-/*
-Copyright (C) 2012 Sebastian Herbord. All rights reserved.
-
-This file is part of Mod Organizer.
-
-Mod Organizer is free software: you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation, either version 3 of the License, or
-(at your option) any later version.
-
-Mod Organizer is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
-*/
-
-#include "lockeddialogbase.h"
-
-#include <QPoint>
-#include <QResizeEvent>
-#include <QWidget>
-#include <Qt> // for Qt::FramelessWindowHint, etc
-
-LockedDialogBase::LockedDialogBase(QWidget *parent, bool allowClose)
- : QDialog(parent)
- , m_Unlocked(false)
- , m_Canceled(false)
- , m_allowClose(allowClose)
-{
- if (parent != nullptr) {
- QPoint position = parent->mapToGlobal(QPoint(parent->width() / 2, parent->height() / 2));
- position.rx() -= this->width() / 2;
- position.ry() -= this->height() / 2;
- move(position);
- }
-}
-
-void LockedDialogBase::resizeEvent(QResizeEvent *event)
-{
- QWidget *par = parentWidget();
- if (par != nullptr) {
- QPoint position = par->mapToGlobal(QPoint(par->width() / 2, par->height() / 2));
- position.rx() -= event->size().width() / 2;
- position.ry() -= event->size().height() / 2;
- move(position);
- }
-}
-
-void LockedDialogBase::reject()
-{
- if (m_allowClose)
- unlock();
-}
-
-bool LockedDialogBase::unlockForced() const {
- return m_Unlocked;
-}
-
-bool LockedDialogBase::canceled() const {
- return m_Canceled;
-}
-
-void LockedDialogBase::unlock() {
- m_Unlocked = true;
-}
-
-void LockedDialogBase::cancel() {
- m_Canceled = true;
-}
-
diff --git a/src/lockeddialogbase.h b/src/lockeddialogbase.h
deleted file mode 100644
index 3c974a38..00000000
--- a/src/lockeddialogbase.h
+++ /dev/null
@@ -1,62 +0,0 @@
-/*
-Copyright (C) 2012 Sebastian Herbord. All rights reserved.
-
-This file is part of Mod Organizer.
-
-Mod Organizer is free software: you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation, either version 3 of the License, or
-(at your option) any later version.
-
-Mod Organizer is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
-*/
-
-#pragma once
-
-#include "ilockedwaitingforprocess.h"
-#include <QDialog> // for QDialog
-#include <QObject> // for Q_OBJECT, slots
-#include <QString> // for QString
-
-class QResizeEvent;
-class QWidget;
-
-/**
- * a small borderless dialog displayed while the Mod Organizer UI is locked
- * The dialog contains only a label and a button to force the UI to be unlocked
- *
- * The UI gets locked while running external applications since they may modify the
- * data on which Mod Organizer works. After the UI is unlocked (manually or after the
- * external application closed) MO will refresh all of its data sources
- **/
-class LockedDialogBase : public QDialog, public ILockedWaitingForProcess
-{
- Q_OBJECT
-
-public:
- explicit LockedDialogBase(QWidget *parent, bool allowClose);
-
- bool unlockForced() const override;
-
- virtual bool canceled() const;
-
-protected:
-
- virtual void resizeEvent(QResizeEvent *event);
-
- virtual void reject();
-
- virtual void unlock();
-
- virtual void cancel();
-
- bool m_Unlocked;
- bool m_Canceled;
- bool m_allowClose;
-};
diff --git a/src/logbuffer.cpp b/src/logbuffer.cpp
deleted file mode 100644
index dfe8f943..00000000
--- a/src/logbuffer.cpp
+++ /dev/null
@@ -1,278 +0,0 @@
-/*
-Copyright (C) 2012 Sebastian Herbord. All rights reserved.
-
-This file is part of Mod Organizer.
-
-Mod Organizer is free software: you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation, either version 3 of the License, or
-(at your option) any later version.
-
-Mod Organizer is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
-*/
-
-#include "logbuffer.h"
-#include <scopeguard.h>
-#include <report.h>
-#include <QMutexLocker>
-#include <QFile>
-#include <QIcon>
-#include <QDateTime>
-#include <Windows.h>
-
-using MOBase::reportError;
-
-QScopedPointer<LogBuffer> LogBuffer::s_Instance;
-QMutex LogBuffer::s_Mutex;
-
-LogBuffer::LogBuffer(int messageCount, QtMsgType minMsgType,
- const QString &outputFileName)
- : QAbstractItemModel(nullptr)
- , m_OutFileName(outputFileName)
- , m_ShutDown(false)
- , m_MinMsgType(minMsgType)
- , m_NumMessages(0)
-{
- m_Messages.resize(messageCount);
-}
-
-LogBuffer::~LogBuffer()
-{
- qInstallMessageHandler(0);
- write();
-}
-
-void LogBuffer::logMessage(QtMsgType type, const QString &message)
-{
- if (type >= m_MinMsgType) {
- QStringList messagelist = message.split("\n");
- for (auto split_message : messagelist) {
- Message msg = {type, QTime::currentTime(), split_message};
- if (m_NumMessages < m_Messages.size()) {
- beginInsertRows(QModelIndex(), static_cast<int>(m_NumMessages),
- static_cast<int>(m_NumMessages) + 1);
- }
- m_Messages.at(m_NumMessages % m_Messages.size()) = msg;
- if (m_NumMessages < m_Messages.size()) {
- endInsertRows();
- } else {
- emit dataChanged(createIndex(0, 0),
- createIndex(static_cast<int>(m_Messages.size()), 0));
- }
- ++m_NumMessages;
- if (type >= QtCriticalMsg) {
- write();
- }
- }
- }
-}
-
-void LogBuffer::write() const
-{
- if (m_NumMessages == 0) {
- return;
- }
-
- DWORD lastError = ::GetLastError();
-
- QFile file(m_OutFileName);
- if (!file.open(QIODevice::WriteOnly)) {
- reportError(tr("failed to write log to %1: %2")
- .arg(m_OutFileName)
- .arg(file.errorString()));
- return;
- }
-
- unsigned int i
- = (m_NumMessages > m_Messages.size())
- ? static_cast<unsigned int>(m_NumMessages - m_Messages.size())
- : 0U;
- for (; i < m_NumMessages; ++i) {
- file.write(m_Messages.at(i % m_Messages.size()).toString().toUtf8());
- file.write("\r\n");
- }
- ::SetLastError(lastError);
-}
-
-void LogBuffer::init(int messageCount, QtMsgType minMsgType,
- const QString &outputFileName)
-{
- QMutexLocker guard(&s_Mutex);
-
- s_Instance.reset(new LogBuffer(messageCount, minMsgType, outputFileName));
- qInstallMessageHandler(LogBuffer::log);
-}
-
-char LogBuffer::msgTypeID(QtMsgType type)
-{
- switch (type) {
- case QtDebugMsg:
- return 'D';
- case QtInfoMsg:
- return 'I';
- case QtWarningMsg:
- return 'W';
- case QtCriticalMsg:
- return 'C';
- case QtFatalMsg:
- return 'F';
- default:
- return '?';
- }
-}
-
-void LogBuffer::log(QtMsgType type, const QMessageLogContext &context,
- const QString &message)
-{
- // QMutexLocker doesn't support timeout...
- if (!s_Mutex.tryLock(100)) {
- fprintf(stderr, "failed to log: %s", qUtf8Printable(message));
- return;
- }
- ON_BLOCK_EXIT([]() { s_Mutex.unlock(); });
-
- if (!s_Instance.isNull()) {
- s_Instance->logMessage(type, message);
- }
-
- if (type == QtDebugMsg) {
- fprintf(stdout, "%s [%c] %s\n", qUtf8Printable(QTime::currentTime().toString()),
- msgTypeID(type), qUtf8Printable(message));
- } else {
- if (context.line != 0) {
- fprintf(stdout, "%s [%c] (%s:%u) %s\n",
- qUtf8Printable(QTime::currentTime().toString()), msgTypeID(type),
- context.file, context.line, qUtf8Printable(message));
- } else {
- fprintf(stdout, "%s [%c] %s\n",
- qUtf8Printable(QTime::currentTime().toString()), msgTypeID(type),
- qUtf8Printable(message));
- }
- }
- fflush(stdout);
-}
-
-QModelIndex LogBuffer::index(int row, int column, const QModelIndex &) const
-{
- return createIndex(row, column, row);
-}
-
-QModelIndex LogBuffer::parent(const QModelIndex &) const
-{
- return QModelIndex();
-}
-
-int LogBuffer::rowCount(const QModelIndex &parent) const
-{
- if (parent.isValid())
- return 0;
- else
- return static_cast<int>(std::min(m_NumMessages, m_Messages.size()));
-}
-
-int LogBuffer::columnCount(const QModelIndex &) const
-{
- return 2;
-}
-
-QVariant LogBuffer::data(const QModelIndex &index, int role) const
-{
- unsigned int offset
- = m_NumMessages < m_Messages.size()
- ? 0
- : static_cast<unsigned int>(m_NumMessages - m_Messages.size());
- unsigned int msgIndex = (offset + index.row() + 1) % m_Messages.size();
- switch (role) {
- case Qt::DisplayRole: {
- if (index.column() == 0) {
- return m_Messages[msgIndex].time.toString("H: mm: ss");
- } else if (index.column() == 1) {
- const QString &msg = m_Messages[msgIndex].message;
- if (msg.length() < 200) {
- return msg;
- } else {
- return msg.mid(0, 200) + "...";
- }
- }
- } break;
- case Qt::DecorationRole: {
- if (index.column() == 1) {
- switch (m_Messages[msgIndex].type) {
- case QtDebugMsg:
- case QtInfoMsg:
- return QIcon(":/MO/gui/information");
- case QtWarningMsg:
- return QIcon(":/MO/gui/warning");
- case QtCriticalMsg:
- return QIcon(":/MO/gui/important");
- case QtFatalMsg:
- return QIcon(":/MO/gui/problem");
- }
- }
- } break;
- case Qt::UserRole: {
- if (index.column() == 1) {
- switch (m_Messages[msgIndex].type) {
- case QtDebugMsg:
- return "D";
- case QtInfoMsg:
- return "I";
- case QtWarningMsg:
- return "W";
- case QtCriticalMsg:
- return "C";
- case QtFatalMsg:
- return "F";
- }
- }
- } break;
- }
- return QVariant();
-}
-
-void LogBuffer::writeNow()
-{
- QMutexLocker guard(&s_Mutex);
- if (!s_Instance.isNull()) {
- s_Instance->write();
- }
-}
-
-void LogBuffer::cleanQuit()
-{
- QMutexLocker guard(&s_Mutex);
- if (!s_Instance.isNull()) {
- s_Instance->m_ShutDown = true;
- }
-}
-
-void log(const char *format, ...)
-{
- va_list argList;
- va_start(argList, format);
-
- static const int BUFFERSIZE = 1000;
-
- char buffer[BUFFERSIZE + 1];
- buffer[BUFFERSIZE] = '\0';
-
- vsnprintf(buffer, BUFFERSIZE, format, argList);
-
- qCritical("%s", buffer);
-
- va_end(argList);
-}
-
-QString LogBuffer::Message::toString() const
-{
- return QString("%1 [%2] %3")
- .arg(time.toString())
- .arg(msgTypeID(type))
- .arg(message);
-}
diff --git a/src/logbuffer.h b/src/logbuffer.h
deleted file mode 100644
index 0cfecfa2..00000000
--- a/src/logbuffer.h
+++ /dev/null
@@ -1,95 +0,0 @@
-/*
-Copyright (C) 2012 Sebastian Herbord. All rights reserved.
-
-This file is part of Mod Organizer.
-
-Mod Organizer is free software: you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation, either version 3 of the License, or
-(at your option) any later version.
-
-Mod Organizer is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
-*/
-
-#ifndef LOGBUFFER_H
-#define LOGBUFFER_H
-
-#include <QObject>
-#include <QMutex>
-#include <QScopedPointer>
-#include <QStringListModel>
-#include <QTime>
-#include <vector>
-
-
-class LogBuffer : public QAbstractItemModel
-{
- Q_OBJECT
-
-public:
-
- static void init(int messageCount, QtMsgType minMsgType, const QString &outputFileName);
- static void log(QtMsgType type, const QMessageLogContext &context, const QString &message);
-
- static void writeNow();
- static void cleanQuit();
-
- static LogBuffer *instance() { return s_Instance.data(); }
-
-public:
-
- virtual ~LogBuffer();
-
- void logMessage(QtMsgType type, const QString &message);
-
- // QAbstractItemModel interface
-public:
- QModelIndex index(int row, int column, const QModelIndex &parent) const;
- QModelIndex parent(const QModelIndex &child) const;
- int rowCount(const QModelIndex &parent) const;
- int columnCount(const QModelIndex &parent) const;
- QVariant data(const QModelIndex &index, int role) const;
-
-signals:
-
-public slots:
-
-private:
-
- explicit LogBuffer(int messageCount, QtMsgType minMsgType, const QString &outputFileName);
- LogBuffer(const LogBuffer &reference); // not implemented
- LogBuffer &operator=(const LogBuffer &reference); // not implemented
-
- void write() const;
-
- static char msgTypeID(QtMsgType type);
-
-private:
-
- struct Message {
- QtMsgType type;
- QTime time;
- QString message;
- QString toString() const;
- };
-
-private:
-
- static QScopedPointer<LogBuffer> s_Instance;
- static QMutex s_Mutex;
-
- QString m_OutFileName;
- bool m_ShutDown;
- QtMsgType m_MinMsgType;
- size_t m_NumMessages;
- std::vector<Message> m_Messages;
-
-};
-
-#endif // LOGBUFFER_H
diff --git a/src/loglist.cpp b/src/loglist.cpp
new file mode 100644
index 00000000..c884be49
--- /dev/null
+++ b/src/loglist.cpp
@@ -0,0 +1,255 @@
+/*
+Copyright (C) 2012 Sebastian Herbord. All rights reserved.
+
+This file is part of Mod Organizer.
+
+Mod Organizer is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+Mod Organizer is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
+*/
+
+#include "loglist.h"
+#include "organizercore.h"
+
+using namespace MOBase;
+
+static LogModel* g_instance = nullptr;
+const std::size_t MaxLines = 1000;
+
+LogModel::LogModel()
+{
+ connect(this, &LogModel::entryAdded, [&](auto&& e){ onEntryAdded(e); });
+}
+
+void LogModel::create()
+{
+ g_instance = new LogModel;
+}
+
+LogModel& LogModel::instance()
+{
+ return *g_instance;
+}
+
+void LogModel::add(MOBase::log::Entry e)
+{
+ emit entryAdded(std::move(e));
+}
+
+void LogModel::clear()
+{
+ beginResetModel();
+ m_entries.clear();
+ endResetModel();
+}
+
+const std::deque<MOBase::log::Entry>& LogModel::entries() const
+{
+ return m_entries;
+}
+
+void LogModel::onEntryAdded(MOBase::log::Entry e)
+{
+ bool full = false;
+ if (m_entries.size() > MaxLines) {
+ m_entries.pop_front();
+ full = true;
+ }
+
+ const int row = static_cast<int>(m_entries.size());
+
+ if (!full) {
+ beginInsertRows(QModelIndex(), row, row + 1);
+ }
+
+ m_entries.emplace_back(std::move(e));
+
+ if (!full) {
+ endInsertRows();
+ } else {
+ emit dataChanged(
+ createIndex(row, 0),
+ createIndex(row + 1, columnCount({})));
+ }
+}
+
+QModelIndex LogModel::index(int row, int column, const QModelIndex&) const
+{
+ return createIndex(row, column, row);
+}
+
+QModelIndex LogModel::parent(const QModelIndex&) const
+{
+ return QModelIndex();
+}
+
+int LogModel::rowCount(const QModelIndex& parent) const
+{
+ if (parent.isValid())
+ return 0;
+ else
+ return static_cast<int>(m_entries.size());
+}
+
+int LogModel::columnCount(const QModelIndex&) const
+{
+ return 3;
+}
+
+QVariant LogModel::data(const QModelIndex& index, int role) const
+{
+ using namespace std::chrono;
+
+ const auto row = static_cast<std::size_t>(index.row());
+ if (row >= m_entries.size()) {
+ return {};
+ }
+
+ const auto& e = m_entries[row];
+
+ if (role == Qt::DisplayRole) {
+ if (index.column() == 1) {
+ const auto ms = duration_cast<milliseconds>(e.time.time_since_epoch());
+ const auto s = duration_cast<seconds>(ms);
+
+ const std::time_t tt = s.count();
+ const int frac = static_cast<int>(ms.count() % 1000);
+
+ const auto time = QDateTime::fromTime_t(tt).time().addMSecs(frac);
+ return time.toString("hh:mm:ss.zzz");
+ } else if (index.column() == 2) {
+ return QString::fromStdString(e.message);
+ }
+ }
+
+ if (role == Qt::DecorationRole) {
+ if (index.column() == 0) {
+ switch (e.level) {
+ case log::Warning:
+ return QIcon(":/MO/gui/warning");
+
+ case log::Error:
+ return QIcon(":/MO/gui/problem");
+
+ case log::Debug: // fall-through
+ case log::Info:
+ default:
+ return {};
+ }
+ }
+ }
+
+ return QVariant();
+}
+
+QVariant LogModel::headerData(int, Qt::Orientation, int) const
+{
+ return {};
+}
+
+
+LogList::LogList(QWidget* parent)
+ : QTreeView(parent), m_core(nullptr)
+{
+ setModel(&LogModel::instance());
+
+ const QFontMetrics fm(font());
+ const int timestampWidth = fm.horizontalAdvance("00:00:00.000");
+
+ header()->setMinimumSectionSize(0);
+ header()->resizeSection(0, 20);
+ header()->resizeSection(1, timestampWidth + 8);
+
+ setAutoScroll(true);
+ scrollToBottom();
+
+ connect(
+ this, &QWidget::customContextMenuRequested,
+ [&](auto&& pos){ onContextMenu(pos); });
+
+ connect(
+ model(), SIGNAL(rowsInserted(const QModelIndex &, int, int)),
+ this, SLOT(scrollToBottom()));
+
+ connect(
+ model(), SIGNAL(dataChanged(QModelIndex,QModelIndex)),
+ this, SLOT(scrollToBottom()));
+}
+
+void LogList::setCore(OrganizerCore& core)
+{
+ m_core = &core;
+}
+
+void LogList::copyToClipboard()
+{
+ std::string s;
+
+ auto* m = static_cast<LogModel*>(model());
+ for (const auto& e : m->entries()) {
+ s += e.formattedMessage + "\n";
+ }
+
+ if (!s.empty()) {
+ // last newline
+ s.pop_back();
+ }
+
+ QApplication::clipboard()->setText(QString::fromStdString(s));
+}
+
+void LogList::clear()
+{
+ static_cast<LogModel*>(model())->clear();
+}
+
+QMenu* LogList::createMenu(QWidget* parent)
+{
+ auto* menu = new QMenu(parent);
+
+ menu->addAction(tr("&Copy all"), [&]{ copyToClipboard(); });
+ menu->addSeparator();
+ menu->addAction(tr("C&lear all"), [&]{ clear(); });
+
+ auto* levels = new QMenu(tr("&Level"));
+ menu->addMenu(levels);
+
+ auto* ag = new QActionGroup(menu);
+
+ auto addAction = [&](auto&& text, auto&& level) {
+ auto* a = new QAction(text, ag);
+
+ a->setCheckable(true);
+ a->setChecked(log::getDefault().level() == level);
+
+ connect(a, &QAction::triggered, [this, level]{
+ if (m_core) {
+ m_core->setLogLevel(level);
+ }
+ });
+
+ levels->addAction(a);
+ };
+
+ addAction(tr("&Debug"), log::Debug);
+ addAction(tr("&Info"), log::Info);
+ addAction(tr("&Warnings"), log::Warning);
+ addAction(tr("&Errors"), log::Error);
+
+ return menu;
+}
+
+void LogList::onContextMenu(const QPoint& pos)
+{
+ auto* menu = createMenu(this);
+ menu->popup(viewport()->mapToGlobal(pos));
+}
diff --git a/src/loglist.h b/src/loglist.h
new file mode 100644
index 00000000..36671be4
--- /dev/null
+++ b/src/loglist.h
@@ -0,0 +1,81 @@
+/*
+Copyright (C) 2012 Sebastian Herbord. All rights reserved.
+
+This file is part of Mod Organizer.
+
+Mod Organizer is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+Mod Organizer is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
+*/
+
+#ifndef LOGBUFFER_H
+#define LOGBUFFER_H
+
+#include <QTreeView>
+#include <log.h>
+
+class OrganizerCore;
+
+class LogModel : public QAbstractItemModel
+{
+ Q_OBJECT
+
+public:
+ static void create();
+ static LogModel& instance();
+
+ void add(MOBase::log::Entry e);
+ void clear();
+
+ const std::deque<MOBase::log::Entry>& entries() const;
+
+protected:
+ QModelIndex index(int row, int column, const QModelIndex& parent) const override;
+ QModelIndex parent(const QModelIndex &child) const override;
+ int rowCount(const QModelIndex &parent) const override;
+ int columnCount(const QModelIndex &parent) const override;
+ QVariant data(const QModelIndex &index, int role) const override;
+
+ QVariant headerData(
+ int section, Qt::Orientation ori, int role=Qt::DisplayRole) const override;
+
+signals:
+ void entryAdded(MOBase::log::Entry e);
+
+private:
+ std::deque<MOBase::log::Entry> m_entries;
+
+ LogModel();
+ void onEntryAdded(MOBase::log::Entry e);
+};
+
+
+class LogList : public QTreeView
+{
+ Q_OBJECT;
+
+public:
+ LogList(QWidget* parent=nullptr);
+
+ void setCore(OrganizerCore& core);
+
+ void copyToClipboard();
+ void clear();
+
+ QMenu* createMenu(QWidget* parent=nullptr);
+
+private:
+ OrganizerCore* m_core;
+ void onContextMenu(const QPoint& pos);
+};
+
+#endif // LOGBUFFER_H
diff --git a/src/loot.cpp b/src/loot.cpp
new file mode 100644
index 00000000..3feb95c9
--- /dev/null
+++ b/src/loot.cpp
@@ -0,0 +1,952 @@
+#include "loot.h"
+#include "lootdialog.h"
+#include "spawn.h"
+#include "organizercore.h"
+#include "json.h"
+#include <log.h>
+#include <report.h>
+
+using namespace MOBase;
+using namespace json;
+
+static QString LootReportPath = QDir::temp().absoluteFilePath("lootreport.json");
+static const DWORD PipeTimeout = 500;
+
+
+class AsyncPipe
+{
+public:
+ AsyncPipe()
+ : m_ioPending(false)
+ {
+ std::fill(std::begin(m_buffer), std::end(m_buffer), 0);
+ std::memset(&m_ov, 0, sizeof(m_ov));
+ }
+
+ env::HandlePtr create()
+ {
+ // creating pipe
+ env::HandlePtr out(createPipe());
+ if (out.get() == INVALID_HANDLE_VALUE) {
+ return {};
+ }
+
+ HANDLE readEventHandle = ::CreateEvent(nullptr, TRUE, FALSE, nullptr);
+
+ if (readEventHandle == NULL) {
+ const auto e = GetLastError();
+ log::error("CreateEvent failed for loot, {}", formatSystemMessage(e));
+ return {};
+ }
+
+ m_ov.hEvent = readEventHandle;
+ m_readEvent.reset(readEventHandle);
+
+ return out;
+ }
+
+ std::string read()
+ {
+ if (m_ioPending) {
+ return checkPending();
+ } else {
+ return tryRead();
+ }
+ }
+
+private:
+ static const std::size_t bufferSize = 50'000;
+
+ env::HandlePtr m_stdout;
+ env::HandlePtr m_readEvent;
+ char m_buffer[bufferSize];
+ OVERLAPPED m_ov;
+ bool m_ioPending;
+
+ HANDLE createPipe()
+ {
+ static const wchar_t* PipeName = L"\\\\.\\pipe\\lootcli_pipe";
+
+ SECURITY_ATTRIBUTES sa = {};
+ sa.nLength = sizeof(SECURITY_ATTRIBUTES);
+ sa.bInheritHandle = TRUE;
+
+ env::HandlePtr pipe;
+
+ // creating pipe
+ {
+ HANDLE pipeHandle = ::CreateNamedPipe(
+ PipeName, PIPE_ACCESS_DUPLEX|FILE_FLAG_OVERLAPPED,
+ PIPE_TYPE_BYTE|PIPE_READMODE_BYTE|PIPE_WAIT,
+ 1, 50'000, 50'000, PipeTimeout, &sa);
+
+ if (pipeHandle == INVALID_HANDLE_VALUE) {
+ const auto e = GetLastError();
+ log::error("CreateNamedPipe failed, {}", formatSystemMessage(e));
+ return INVALID_HANDLE_VALUE;
+ }
+
+ pipe.reset(pipeHandle);
+ }
+
+ {
+ // duplicating the handle to read from it
+ HANDLE outputRead = INVALID_HANDLE_VALUE;
+
+ const auto r = DuplicateHandle(
+ GetCurrentProcess(), pipe.get(), GetCurrentProcess(), &outputRead,
+ 0, TRUE, DUPLICATE_SAME_ACCESS);
+
+ if (!r) {
+ const auto e = GetLastError();
+ log::error("DuplicateHandle for pipe failed, {}", formatSystemMessage(e));
+ return INVALID_HANDLE_VALUE;
+ }
+
+ m_stdout.reset(outputRead);
+ }
+
+
+ // creating handle to pipe which is passed to CreateProcess()
+ HANDLE outputWrite = ::CreateFileW(
+ PipeName, FILE_WRITE_DATA|SYNCHRONIZE, 0,
+ &sa, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0);
+
+ if (outputWrite == INVALID_HANDLE_VALUE) {
+ const auto e = GetLastError();
+ log::error("CreateFileW for pipe failed, {}", formatSystemMessage(e));
+ return INVALID_HANDLE_VALUE;
+ }
+
+ return outputWrite;
+ }
+
+ std::string tryRead()
+ {
+ DWORD bytesRead = 0;
+
+ if (!::ReadFile(m_stdout.get(), m_buffer, bufferSize, &bytesRead, &m_ov)) {
+ const auto e = GetLastError();
+
+ switch (e)
+ {
+ case ERROR_IO_PENDING:
+ {
+ m_ioPending = true;
+ break;
+ }
+
+ case ERROR_BROKEN_PIPE:
+ {
+ // broken pipe probably means lootcli is finished
+ break;
+ }
+
+ default:
+ {
+ log::error("{}", formatSystemMessage(e));
+ break;
+ }
+ }
+
+ return {};
+ }
+
+ return {m_buffer, m_buffer + bytesRead};
+ }
+
+ std::string checkPending()
+ {
+ DWORD bytesRead = 0;
+
+ const auto r = WaitForSingleObject(m_readEvent.get(), PipeTimeout);
+
+ if (r == WAIT_FAILED) {
+ const auto e = GetLastError();
+ log::error("WaitForSingleObject in AsyncPipe failed, {}", formatSystemMessage(e));
+ return {};
+ }
+
+ if (!::GetOverlappedResult(m_stdout.get(), &m_ov, &bytesRead, FALSE)) {
+ const auto e = GetLastError();
+
+ switch (e)
+ {
+ case ERROR_IO_INCOMPLETE:
+ {
+ break;
+ }
+
+ case WAIT_TIMEOUT:
+ {
+ break;
+ }
+
+ case ERROR_BROKEN_PIPE:
+ {
+ // broken pipe probably means lootcli is finished
+ break;
+ }
+
+ default:
+ {
+ log::error("GetOverlappedResult failed, {}", formatSystemMessage(e));
+ break;
+ }
+ }
+
+ return {};
+ }
+
+ ::ResetEvent(m_readEvent.get());
+ m_ioPending = false;
+
+ return {m_buffer, m_buffer + bytesRead};
+ }
+};
+
+
+
+log::Levels levelFromLoot(lootcli::LogLevels level)
+{
+ using LC = lootcli::LogLevels;
+
+ switch (level)
+ {
+ case LC::Trace: // fall-through
+ case LC::Debug:
+ return log::Debug;
+
+ case LC::Info:
+ return log::Info;
+
+ case LC::Warning:
+ return log::Warning;
+
+ case LC::Error:
+ return log::Error;
+
+ default:
+ return log::Info;
+ }
+}
+
+QString Loot::Report::toMarkdown() const
+{
+ QString s;
+
+ if (!okay) {
+ s += "## " + tr("Loot failed to run") + "\n";
+
+ if (errors.empty() && warnings.empty()) {
+ s += tr("No errors were reported. The log below might have more information.\n");
+ }
+ }
+
+ s += errorsMarkdown();
+
+ if (okay) {
+ s += "\n" + successMarkdown();
+ }
+
+ return s;
+}
+
+QString Loot::Report::successMarkdown() const
+{
+ QString s;
+
+ if (!messages.empty()) {
+ s += "### " + QObject::tr("General messages") + "\n";
+
+ for (auto&& m : messages) {
+ s += " - " + m.toMarkdown() + "\n";
+ }
+ }
+
+ if (!plugins.empty()) {
+ if (!s.isEmpty()) {
+ s += "\n";
+ }
+
+ s += "### " + QObject::tr("Plugins") + "\n";
+
+ for (auto&& p : plugins) {
+ const auto ps = p.toMarkdown();
+ if (!ps.isEmpty()) {
+ s += ps + "\n";
+ }
+ }
+ }
+
+ if (s.isEmpty()) {
+ s += "**" + QObject::tr("No messages.") + "**\n";
+ }
+
+ s += stats.toMarkdown();
+
+ return s;
+}
+
+QString Loot::Report::errorsMarkdown() const
+{
+ QString s;
+
+ if (!errors.empty()) {
+ s += "### " + tr("Errors") + ":\n";
+
+ for (auto&& e : errors) {
+ s += " - " + e + "\n";
+ }
+ }
+
+ if (!warnings.empty()) {
+ if (!s.isEmpty()) {
+ s += "\n";
+ }
+
+ s += "### " + tr("Warnings") + ":\n";
+
+ for (auto&& w : warnings) {
+ s += " - " + w + "\n";
+ }
+ }
+
+ return s;
+}
+
+QString Loot::Stats::toMarkdown() const
+{
+ return QString("`stats: %1s, lootcli %2, loot %3`")
+ .arg(QString::number(time / 1000.0, 'f', 2))
+ .arg(lootcliVersion)
+ .arg(lootVersion);
+}
+
+QString Loot::Plugin::toMarkdown() const
+{
+ QString s;
+
+ if (!incompatibilities.empty()) {
+ s += " - **" + QObject::tr("Incompatibilities") + ": ";
+
+ QString fs;
+ for (auto&& f : incompatibilities) {
+ if (!fs.isEmpty()) {
+ fs += ", ";
+ }
+
+ fs += f.displayName.isEmpty() ? f.name : f.displayName;
+ }
+
+ s += fs + "**\n";
+ }
+
+ if (!missingMasters.empty()) {
+ s += " - **" + QObject::tr("Missing masters") + ": ";
+
+ QString ms;
+ for (auto&& m : missingMasters) {
+ if (!ms.isEmpty()) {
+ ms += ", ";
+ }
+
+ ms += m;
+ }
+
+ s += ms + "**\n";
+ }
+
+ for (auto&& m : messages) {
+ s += " - " + m.toMarkdown() + "\n";
+ }
+
+ for (auto&& d : dirty) {
+ s += " - " + d.toMarkdown(false) + "\n";
+ }
+
+ if (!s.isEmpty()) {
+ s = "#### " + name + "\n" + s;
+ }
+
+ return s;
+}
+
+QString Loot::Dirty::toString(bool isClean) const
+{
+ if (isClean) {
+ return QObject::tr("Verified clean by %1")
+ .arg(cleaningUtility.isEmpty() ? "?" : cleaningUtility);
+ }
+
+ QString s = cleaningString();
+
+ if (!info.isEmpty()) {
+ s += " " + info;
+ }
+
+ return s;
+}
+
+QString Loot::Dirty::toMarkdown(bool isClean) const
+{
+ return toString(isClean);
+}
+
+QString Loot::Dirty::cleaningString() const
+{
+ return QObject::tr("%1 found %2 ITM record(s), %3 deleted reference(s) and %4 deleted navmesh(es).")
+ .arg(cleaningUtility.isEmpty() ? "?" : cleaningUtility)
+ .arg(itm)
+ .arg(deletedReferences)
+ .arg(deletedNavmesh);
+}
+
+QString Loot::Message::toMarkdown() const
+{
+ QString s;
+
+ switch (type)
+ {
+ case log::Error:
+ {
+ s += "**" + QObject::tr("Error") + "**: ";
+ break;
+ }
+
+ case log::Warning:
+ {
+ s += "**" + QObject::tr("Warning") + "**: ";
+ break;
+ }
+
+ default:
+ {
+ break;
+ }
+ }
+
+ s += text;
+
+ return s;
+}
+
+
+Loot::Loot(OrganizerCore& core)
+ : m_core(core), m_thread(nullptr), m_cancel(false), m_result(false)
+{
+}
+
+Loot::~Loot()
+{
+ if (m_thread) {
+ m_thread->wait();
+ }
+
+ deleteReportFile();
+}
+
+bool Loot::start(QWidget* parent, bool didUpdateMasterList)
+{
+ deleteReportFile();
+
+ log::debug("starting loot");
+
+ m_pipe.reset(new AsyncPipe);
+
+ env::HandlePtr stdoutHandle = m_pipe->create();
+ if (!stdoutHandle) {
+ return false;
+ }
+
+ // vfs
+ m_core.prepareVFS();
+
+ // spawning
+ if (!spawnLootcli(parent, didUpdateMasterList, std::move(stdoutHandle))) {
+ return false;
+ }
+
+ // starting thread
+ log::debug("starting loot thread");
+ m_thread.reset(QThread::create([&]{ lootThread(); }));
+ m_thread->start();
+
+ return true;
+}
+
+bool Loot::spawnLootcli(
+ QWidget* parent, bool didUpdateMasterList, env::HandlePtr stdoutHandle)
+{
+ const auto logLevel = m_core.settings().diagnostics().lootLogLevel();
+
+ QStringList parameters;
+ parameters
+ << "--game"
+ << m_core.managedGame()->gameShortName()
+
+ << "--gamePath"
+ << QString("\"%1\"").arg(m_core.managedGame()->gameDirectory().absolutePath())
+
+ << "--pluginListPath"
+ << QString("\"%1/loadorder.txt\"").arg(m_core.profilePath())
+
+ << "--logLevel"
+ << QString::fromStdString(lootcli::logLevelToString(logLevel))
+
+ << "--out"
+ << QString("\"%1\"").arg(LootReportPath)
+
+ << "--language"
+ << m_core.settings().interface().language();
+
+ if (didUpdateMasterList) {
+ parameters << "--skipUpdateMasterlist";
+ }
+
+ spawn::SpawnParameters sp;
+ sp.binary = QFileInfo(qApp->applicationDirPath() + "/loot/lootcli.exe");
+ sp.arguments = parameters.join(" ");
+ sp.currentDirectory.setPath(qApp->applicationDirPath() + "/loot");
+ sp.hooked = true;
+ sp.stdOut = stdoutHandle.get();
+
+ HANDLE lootHandle = spawn::startBinary(parent, sp);
+
+ if (lootHandle == INVALID_HANDLE_VALUE) {
+ emit log(log::Levels::Error, tr("failed to start loot"));
+ return false;
+ }
+
+ m_lootProcess.reset(lootHandle);
+
+ return true;
+}
+
+void Loot::cancel()
+{
+ if (!m_cancel) {
+ log::debug("loot received cancel request");
+ m_cancel = true;
+ }
+}
+
+bool Loot::result() const
+{
+ return m_result;
+}
+
+const QString& Loot::outPath() const
+{
+ return LootReportPath;
+}
+
+const Loot::Report& Loot::report() const
+{
+ return m_report;
+}
+
+const std::vector<QString>& Loot::errors() const
+{
+ return m_errors;
+}
+
+const std::vector<QString>& Loot::warnings() const
+{
+ return m_warnings;
+}
+
+void Loot::lootThread()
+{
+ try
+ {
+ m_result = false;
+
+ if (waitForCompletion()) {
+ m_result = true;
+ }
+
+ m_report = createReport();
+ }
+ catch(...)
+ {
+ log::error("unhandled exception in loot thread");
+ }
+
+ log::debug("finishing loot thread");
+ emit finished();
+}
+
+
+bool Loot::waitForCompletion()
+{
+ bool terminating = false;
+
+ log::debug("loot thread waiting for completion on lootcli");
+
+ for (;;) {
+ DWORD res = WaitForSingleObject(m_lootProcess.get(), 100);
+
+ if (res == WAIT_OBJECT_0) {
+ log::debug("lootcli has completed");
+ // done
+ break;
+ }
+
+ if (res == WAIT_FAILED) {
+ const auto e = GetLastError();
+ log::error("failed to wait on loot process, {}", formatSystemMessage(e));
+ return false;
+ }
+
+ if (m_cancel) {
+ log::debug("terminating lootcli process");
+ ::TerminateProcess(m_lootProcess.get(), 1);
+
+ log::debug("waiting for loocli process to terminate");
+ WaitForSingleObject(m_lootProcess.get(), INFINITE);
+
+ log::debug("lootcli terminated");
+ return false;
+ }
+
+ processStdout(m_pipe->read());
+ }
+
+ if (m_cancel) {
+ return false;
+ }
+
+ processStdout(m_pipe->read());
+
+ // checking exit code
+ DWORD exitCode = 0;
+
+ if (!::GetExitCodeProcess(m_lootProcess.get(), &exitCode)) {
+ const auto e = GetLastError();
+ log::error("failed to get exit code for loot, {}", formatSystemMessage(e));
+ return false;
+ }
+
+ if (exitCode != 0UL) {
+ emit log(log::Levels::Error, tr("Loot failed. Exit code was: %1").arg(exitCode));
+ return false;
+ }
+
+ return true;
+}
+
+void Loot::processStdout(const std::string &lootOut)
+{
+ emit output(QString::fromStdString(lootOut));
+
+ m_outputBuffer += lootOut;
+ if (m_outputBuffer.empty()) {
+ return;
+ }
+
+ std::size_t start = 0;
+
+ for (;;) {
+ const auto newline = m_outputBuffer.find("\n", start);
+ if (newline == std::string::npos) {
+ break;
+ }
+
+ const std::string_view line(m_outputBuffer.c_str() + start, newline - start);
+ const auto m = lootcli::parseMessage(line);
+
+ if (m.type == lootcli::MessageType::None) {
+ log::error("unrecognised loot output: '{}'", line);
+ continue;
+ }
+
+ processMessage(m);
+
+ start = newline + 1;
+ }
+
+ m_outputBuffer.erase(0, start);
+}
+
+void Loot::processMessage(const lootcli::Message& m)
+{
+ switch (m.type)
+ {
+ case lootcli::MessageType::Log:
+ {
+ const auto level = levelFromLoot(m.logLevel);
+
+ if (level == log::Error) {
+ m_errors.push_back(QString::fromStdString(m.log));
+ } else if (level == log::Warning) {
+ m_warnings.push_back(QString::fromStdString(m.log));
+ }
+
+ emit log(level, QString::fromStdString(m.log));
+ break;
+ }
+
+ case lootcli::MessageType::Progress:
+ {
+ emit progress(m.progress);
+ break;
+ }
+ }
+}
+
+Loot::Report Loot::createReport() const
+{
+ Report r;
+
+ r.okay = m_result;
+ r.errors = m_errors;
+ r.warnings = m_warnings;
+
+ if (m_result) {
+ processOutputFile(r);
+ }
+
+ return r;
+}
+
+void Loot::deleteReportFile()
+{
+ if (QFile::exists(LootReportPath)) {
+ log::debug("deleting temporary loot report '{}'", LootReportPath);
+ const auto r = shell::Delete(LootReportPath);
+
+ if (!r) {
+ log::error(
+ "failed to remove temporary loot json report '{}': {}",
+ LootReportPath, r.toString());
+ }
+ }
+}
+
+void Loot::processOutputFile(Report& r) const
+{
+ log::debug("parsing json output file at '{}'", LootReportPath);
+
+ QFile outFile(LootReportPath);
+ if (!outFile.open(QIODevice::ReadOnly)) {
+ emit log(
+ MOBase::log::Error,
+ QString("failed to open file, %1 (error %2)")
+ .arg(outFile.errorString()).arg(outFile.error()));
+
+ return;
+ }
+
+ QJsonParseError e;
+ const QJsonDocument doc = QJsonDocument::fromJson(outFile.readAll(), &e);
+ if (doc.isNull()) {
+ emit log(
+ MOBase::log::Error,
+ QString("invalid json, %1 (error %2)")
+ .arg(e.errorString()).arg(e.error));
+
+ return;
+ }
+
+ requireObject(doc, "root");
+
+ const QJsonObject object = doc.object();
+
+ r.messages = reportMessages(getOpt<QJsonArray>(object, "messages"));
+ r.plugins = reportPlugins(getOpt<QJsonArray>(object, "plugins"));
+ r.stats = reportStats(getWarn<QJsonObject>(object, "stats"));
+}
+
+std::vector<Loot::Plugin> Loot::reportPlugins(const QJsonArray& plugins) const
+{
+ std::vector<Loot::Plugin> v;
+
+ for (auto pluginValue : plugins) {
+ const auto o = convertWarn<QJsonObject>(pluginValue, "plugin");
+ if (o.isEmpty()) {
+ continue;
+ }
+
+ auto p = reportPlugin(o);
+ if (!p.name.isEmpty()) {
+ v.emplace_back(std::move(p));
+ }
+ }
+
+ return v;
+}
+
+Loot::Plugin Loot::reportPlugin(const QJsonObject& plugin) const
+{
+ Plugin p;
+
+ p.name = getWarn<QString>(plugin, "name");
+ if (p.name.isEmpty()) {
+ return {};
+ }
+
+ // ignore disabled plugins; lootcli doesn't know if a plugin is enabled or not
+ // and will report information on any plugin that's in the filesystem
+ if (!m_core.pluginList()->isEnabled(p.name)) {
+ return {};
+ }
+
+ if (plugin.contains("incompatibilities")) {
+ p.incompatibilities = reportFiles(getOpt<QJsonArray>(plugin, "incompatibilities"));
+ }
+
+ if (plugin.contains("messages")) {
+ p.messages = reportMessages(getOpt<QJsonArray>(plugin, "messages"));
+ }
+
+ if (plugin.contains("dirty")) {
+ p.dirty = reportDirty(getOpt<QJsonArray>(plugin, "dirty"));
+ }
+
+ if (plugin.contains("clean")) {
+ p.clean = reportDirty(getOpt<QJsonArray>(plugin, "clean"));
+ }
+
+ if (plugin.contains("missingMasters")) {
+ p.missingMasters = reportStringArray(getOpt<QJsonArray>(plugin, "missingMasters"));
+ }
+
+ p.loadsArchive = getOpt(plugin, "loadsArchive", false);
+ p.isMaster = getOpt(plugin, "isMaster", false);
+ p.isLightMaster = getOpt(plugin, "isLightMaster", false);
+
+ return p;
+}
+
+Loot::Stats Loot::reportStats(const QJsonObject& stats) const
+{
+ Stats s;
+
+ s.time = getWarn<qint64>(stats, "time");
+ s.lootcliVersion = getWarn<QString>(stats, "lootcliVersion");
+ s.lootVersion = getWarn<QString>(stats, "lootVersion");
+
+ return s;
+}
+
+std::vector<Loot::Message> Loot::reportMessages(const QJsonArray& array) const
+{
+ std::vector<Loot::Message> v;
+
+ for (auto messageValue : array) {
+ const auto o = convertWarn<QJsonObject>(messageValue, "message");
+ if (o.isEmpty()) {
+ continue;
+ }
+
+ Message m;
+
+ const auto type = getWarn<QString>(o, "type");
+
+ if (type == "info") {
+ m.type = log::Info;
+ } else if (type == "warn") {
+ m.type = log::Warning;
+ } else if (type == "error") {
+ m.type = log::Error;
+ } else {
+ log::error("unknown message type '{}'", type);
+ m.type = log::Info;
+ }
+
+ m.text = getWarn<QString>(o, "text");
+
+ if (!m.text.isEmpty()) {
+ v.emplace_back(std::move(m));
+ }
+ }
+
+ return v;
+}
+
+std::vector<Loot::File> Loot::reportFiles(const QJsonArray& array) const
+{
+ std::vector<Loot::File> v;
+
+ for (auto&& fileValue : array) {
+ const auto o = convertWarn<QJsonObject>(fileValue, "file");
+ if (o.isEmpty()) {
+ continue;
+ }
+
+ File f;
+
+ f.name = getWarn<QString>(o, "name");
+ f.displayName = getOpt<QString>(o, "displayName");
+
+ if (!f.name.isEmpty()) {
+ v.emplace_back(std::move(f));
+ }
+ }
+
+ return v;
+}
+
+std::vector<Loot::Dirty> Loot::reportDirty(const QJsonArray& array) const
+{
+ std::vector<Loot::Dirty> v;
+
+ for (auto&& dirtyValue : array) {
+ const auto o = convertWarn<QJsonObject>(dirtyValue, "dirty");
+
+ Dirty d;
+
+ d.crc = getWarn<qint64>(o, "crc");
+ d.itm = getOpt<qint64>(o, "itm");
+ d.deletedReferences = getOpt<qint64>(o, "deletedReferences");
+ d.deletedNavmesh = getOpt<qint64>(o, "deletedNavmesh");
+ d.cleaningUtility = getOpt<QString>(o, "cleaningUtility");
+ d.info = getOpt<QString>(o, "info");
+
+ v.emplace_back(std::move(d));
+ }
+
+ return v;
+}
+
+std::vector<QString> Loot::reportStringArray(const QJsonArray& array) const
+{
+ std::vector<QString> v;
+
+ for (auto&& sv : array) {
+ auto s = convertWarn<QString>(sv, "string");
+ if (s.isEmpty()) {
+ continue;
+ }
+
+ v.emplace_back(std::move(s));
+ }
+
+ return v;
+}
+
+
+bool runLoot(QWidget* parent, OrganizerCore& core, bool didUpdateMasterList)
+{
+ core.savePluginList();
+
+ try {
+ Loot loot(core);
+ LootDialog dialog(parent, core, loot);
+
+ if (!loot.start(parent, didUpdateMasterList)) {
+ return false;
+ }
+
+ dialog.exec();
+
+ return dialog.result();
+ } catch (const UsvfsConnectorException &e) {
+ log::debug("{}", e.what());
+ return false;
+ } catch (const std::exception &e) {
+ reportError(QObject::tr("failed to run loot: %1").arg(e.what()));
+ return false;
+ }
+}
diff --git a/src/loot.h b/src/loot.h
new file mode 100644
index 00000000..f9943626
--- /dev/null
+++ b/src/loot.h
@@ -0,0 +1,144 @@
+#ifndef MODORGANIZER_LOOT_H
+#define MODORGANIZER_LOOT_H
+
+#include "envmodule.h"
+#include <log.h>
+#include <lootcli/lootcli.h>
+#include <windows.h>
+#include <QWidget>
+
+Q_DECLARE_METATYPE(lootcli::Progress);
+Q_DECLARE_METATYPE(MOBase::log::Levels);
+
+class OrganizerCore;
+class AsyncPipe;
+
+class Loot : public QObject
+{
+ Q_OBJECT;
+
+public:
+ struct Message
+ {
+ MOBase::log::Levels type;
+ QString text;
+
+ QString toMarkdown() const;
+ };
+
+ struct File
+ {
+ QString name;
+ QString displayName;
+ };
+
+ struct Dirty
+ {
+ qint64 crc=0;
+ qint64 itm=0;
+ qint64 deletedReferences=0;
+ qint64 deletedNavmesh=0;
+ QString cleaningUtility;
+ QString info;
+
+ QString toString(bool isClean) const;
+ QString toMarkdown(bool isClean) const;
+ QString cleaningString() const;
+ };
+
+ struct Plugin
+ {
+ QString name;
+ std::vector<File> incompatibilities;
+ std::vector<Message> messages;
+ std::vector<Dirty> dirty, clean;
+ std::vector<QString> missingMasters;
+ bool loadsArchive = false;
+ bool isMaster = false;
+ bool isLightMaster = false;
+
+ QString toMarkdown() const;
+ };
+
+ struct Stats
+ {
+ qint64 time = 0;
+ QString lootcliVersion;
+ QString lootVersion;
+
+ QString toMarkdown() const;
+ };
+
+ struct Report
+ {
+ bool okay = false;
+ std::vector<QString> errors, warnings;
+ std::vector<Message> messages;
+ std::vector<Plugin> plugins;
+ Stats stats;
+
+ QString toMarkdown() const;
+
+ private:
+ QString successMarkdown() const;
+ QString errorsMarkdown() const;
+ };
+
+
+ Loot(OrganizerCore& core);
+ ~Loot();
+
+ bool start(QWidget* parent, bool didUpdateMasterList);
+ void cancel();
+ bool result() const;
+
+ const QString& outPath() const;
+ const Report& report() const;
+ const std::vector<QString>& errors() const;
+ const std::vector<QString>& warnings() const;
+
+signals:
+ void output(const QString& s);
+ void progress(const lootcli::Progress p);
+ void log(MOBase::log::Levels level, const QString& s) const;
+ void finished();
+
+private:
+ OrganizerCore& m_core;
+ std::unique_ptr<QThread> m_thread;
+ std::atomic<bool> m_cancel;
+ std::atomic<bool> m_result;
+ env::HandlePtr m_lootProcess;
+ std::unique_ptr<AsyncPipe> m_pipe;
+ std::string m_outputBuffer;
+ std::vector<QString> m_errors, m_warnings;
+ Report m_report;
+
+ bool spawnLootcli(
+ QWidget* parent, bool didUpdateMasterList, env::HandlePtr stdoutHandle);
+
+ void lootThread();
+ bool waitForCompletion();
+
+ void processStdout(const std::string &lootOut);
+ void processMessage(const lootcli::Message& m);
+
+ Report createReport() const;
+ void processOutputFile(Report& r) const;
+ void deleteReportFile();
+
+ Message reportMessage(const QJsonObject& message) const;
+ std::vector<Plugin> reportPlugins(const QJsonArray& plugins) const;
+ Loot::Plugin reportPlugin(const QJsonObject& plugin) const;
+ Loot::Stats reportStats(const QJsonObject& stats) const;
+
+ std::vector<Message> reportMessages(const QJsonArray& array) const;
+ std::vector<Loot::File> reportFiles(const QJsonArray& array) const;
+ std::vector<Loot::Dirty> reportDirty(const QJsonArray& array) const;
+ std::vector<QString> reportStringArray(const QJsonArray& array) const;
+};
+
+
+bool runLoot(QWidget* parent, OrganizerCore& core, bool didUpdateMasterList);
+
+#endif // MODORGANIZER_LOOT_H
diff --git a/src/lootdialog.cpp b/src/lootdialog.cpp
new file mode 100644
index 00000000..5ac65907
--- /dev/null
+++ b/src/lootdialog.cpp
@@ -0,0 +1,283 @@
+#include "lootdialog.h"
+#include "ui_lootdialog.h"
+#include "loot.h"
+#include "organizercore.h"
+#include <utility.h>
+#include <QWebChannel>
+
+using namespace MOBase;
+
+QString progressToString(lootcli::Progress p)
+{
+ using P = lootcli::Progress;
+
+ static const std::map<P, QString> map = {
+ {P::CheckingMasterlistExistence, QObject::tr("Checking masterlist existence")},
+ {P::UpdatingMasterlist, QObject::tr("Updating masterlist")},
+ {P::LoadingLists, QObject::tr("Loading lists")},
+ {P::ReadingPlugins, QObject::tr("Reading plugins")},
+ {P::SortingPlugins, QObject::tr("Sorting plugins")},
+ {P::WritingLoadorder, QObject::tr("Writing loadorder.txt")},
+ {P::ParsingLootMessages, QObject::tr("Parsing loot messages")},
+ {P::Done, QObject::tr("Done")}
+ };
+
+ auto itor = map.find(p);
+ if (itor == map.end()) {
+ return QString("unknown progress %1").arg(static_cast<int>(p));
+ } else {
+ return itor->second;
+ }
+}
+
+
+MarkdownDocument::MarkdownDocument(QObject* parent)
+ : QObject(parent)
+{
+}
+
+void MarkdownDocument::setText(const QString& text)
+{
+ if (m_text == text)
+ return;
+
+ m_text = text;
+ emit textChanged(m_text);
+}
+
+
+MarkdownPage::MarkdownPage(QObject* parent)
+ : QWebEnginePage(parent)
+{
+}
+
+bool MarkdownPage::acceptNavigationRequest(const QUrl &url, NavigationType, bool)
+{
+ static const QStringList allowed = {"qrc", "data"};
+
+ if (!allowed.contains(url.scheme())) {
+ QDesktopServices::openUrl(url);
+ return false;
+ }
+
+ return true;
+}
+
+
+LootDialog::LootDialog(QWidget* parent, OrganizerCore& core, Loot& loot) :
+ QDialog(parent), ui(new Ui::LootDialog), m_core(core), m_loot(loot),
+ m_finished(false), m_cancelling(false)
+{
+ createUI();
+
+ QObject::connect(
+ &m_loot, &Loot::output, this,
+ [&](auto&& s){ addOutput(s); }, Qt::QueuedConnection);
+
+ QObject::connect(
+ &m_loot, &Loot::progress,
+ this, [&](auto&& p){ setProgress(p); }, Qt::QueuedConnection);
+
+ QObject::connect(
+ &m_loot, &Loot::log, this,
+ [&](auto&& lv, auto&& s){ log(lv, s); }, Qt::QueuedConnection);
+
+ QObject::connect(
+ &m_loot, &Loot::finished, this,
+ [&]{ onFinished(); }, Qt::QueuedConnection);
+}
+
+LootDialog::~LootDialog() = default;
+
+void LootDialog::setText(const QString& s)
+{
+ ui->progressText->setText(s);
+}
+
+void LootDialog::setProgress(lootcli::Progress p)
+{
+ // don't overwrite the "stopping loot" message even if lootcli generates a new
+ // progress message
+ if (!m_cancelling) {
+ setText(progressToString(p));
+ }
+
+ if (p == lootcli::Progress::Done) {
+ ui->progressBar->setRange(0, 1);
+ ui->progressBar->setValue(1);
+ }
+}
+
+void LootDialog::addOutput(const QString& s)
+{
+ if (m_core.settings().diagnostics().lootLogLevel() > lootcli::LogLevels::Debug) {
+ return;
+ }
+
+ const auto lines = s.split(QRegExp("[\\r\\n]"), QString::SkipEmptyParts);
+
+ for (auto&& line : lines) {
+ if (line.isEmpty()) {
+ continue;
+ }
+
+ addLineOutput(line);
+ }
+}
+
+bool LootDialog::result() const
+{
+ return m_loot.result();
+}
+
+void LootDialog::cancel()
+{
+ if (!m_finished && !m_cancelling) {
+ log::debug("loot dialog: cancelling");
+ m_loot.cancel();
+
+ setText(tr("Stopping LOOT..."));
+ addLineOutput("stopping loot");
+
+ ui->buttons->setEnabled(false);
+ m_cancelling = true;
+ }
+}
+
+void LootDialog::openReport()
+{
+ const auto path = m_loot.outPath();
+ shell::Open(path);
+}
+
+int LootDialog::exec()
+{
+ auto& s = m_core.settings();
+
+ GeometrySaver gs(s, this);
+ s.geometry().restoreState(&m_expander);
+
+ const auto r = QDialog::exec();
+
+ s.geometry().saveState(&m_expander);
+
+ return r;
+}
+
+void LootDialog::accept()
+{
+ // no-op
+}
+
+void LootDialog::reject()
+{
+ if (m_finished) {
+ log::debug("loot dialog reject: loot finished, closing");
+ QDialog::reject();
+ } else {
+ log::debug("loot dialog reject: not finished, cancelling");
+ cancel();
+ }
+}
+
+void LootDialog::createUI()
+{
+ ui->setupUi(this);
+ ui->progressBar->setMaximum(0);
+
+ auto* page = new MarkdownPage(this);
+ ui->report->setPage(page);
+
+ auto* channel = new QWebChannel(this);
+ channel->registerObject("content", &m_report);
+ page->setWebChannel(channel);
+
+ const QString path = QApplication::applicationDirPath() + "/resources/markdown.html";
+ QFile f(path);
+
+ if (f.open(QFile::ReadOnly)) {
+ const QString html = f.readAll();
+ if (!html.isEmpty()) {
+ ui->report->setHtml(html);
+ } else {
+ log::error("failed to read '{}', {}", path, f.errorString());
+ }
+ } else {
+ log::error("can't open '{}', {}", path, f.errorString());
+ }
+
+ m_expander.set(ui->details, ui->detailsPanel);
+ ui->openJsonReport->setEnabled(false);
+ connect(ui->openJsonReport, &QPushButton::clicked, [&]{ openReport(); });
+
+ ui->buttons->setStandardButtons(QDialogButtonBox::Cancel);
+
+ m_report.setText(tr("Running LOOT..."));
+
+ resize(650, 450);
+}
+
+void LootDialog::closeEvent(QCloseEvent* e)
+{
+ if (m_finished) {
+ log::debug("loot dialog close event: finished, closing");
+ QDialog::closeEvent(e);
+ } else {
+ log::debug("loot dialog close event: not finished, cancelling");
+ cancel();
+ e->ignore();
+ }
+}
+
+void LootDialog::addLineOutput(const QString& line)
+{
+ ui->output->appendPlainText(line);
+}
+
+void LootDialog::onFinished()
+{
+ log::debug("loot dialog: loot is finished");
+
+ m_finished = true;
+
+ if (m_cancelling) {
+ log::debug("loot dialog: was cancelling, closing");
+ close();
+ } else {
+ log::debug("loot dialog: showing report");
+
+ showReport();
+
+ ui->openJsonReport->setEnabled(true);
+ ui->buttons->setStandardButtons(QDialogButtonBox::Close);
+
+ // if loot failed, the Done progress won't be received; this makes sure
+ // the progress bar is stopped
+ setProgress(lootcli::Progress::Done);
+ }
+}
+
+void LootDialog::log(log::Levels lv, const QString& s)
+{
+ if (lv >= log::Levels::Warning) {
+ log::log(lv, "{}", s);
+ }
+
+ if (m_core.settings().diagnostics().lootLogLevel() > lootcli::LogLevels::Debug) {
+ addLineOutput(QString("[%1] %2").arg(log::levelToString(lv)).arg(s));
+ }
+}
+
+void LootDialog::showReport()
+{
+ const auto& lootReport = m_loot.report();
+
+ if (m_loot.result()) {
+ m_core.pluginList()->clearAdditionalInformation();
+ for (auto&& p : lootReport.plugins) {
+ m_core.pluginList()->addLootReport(p.name, p);
+ }
+ }
+
+ m_report.setText(lootReport.toMarkdown());
+}
diff --git a/src/lootdialog.h b/src/lootdialog.h
new file mode 100644
index 00000000..3cec15c6
--- /dev/null
+++ b/src/lootdialog.h
@@ -0,0 +1,79 @@
+#ifndef MODORGANIZER_LOOTDIALOG_H
+#define MODORGANIZER_LOOTDIALOG_H
+
+#include <lootcli/lootcli.h>
+#include <log.h>
+#include <expanderwidget.h>
+
+namespace Ui { class LootDialog; }
+
+class OrganizerCore;
+class Loot;
+
+
+class MarkdownDocument : public QObject
+{
+ Q_OBJECT;
+ Q_PROPERTY(QString text MEMBER m_text NOTIFY textChanged FINAL);
+
+public:
+ explicit MarkdownDocument(QObject* parent=nullptr);
+ void setText(const QString& text);
+
+signals:
+ void textChanged(const QString &text);
+
+private:
+ QString m_text;
+};
+
+
+class MarkdownPage : public QWebEnginePage
+{
+ Q_OBJECT;
+
+public:
+ explicit MarkdownPage(QObject* parent=nullptr);
+
+protected:
+ bool acceptNavigationRequest(const QUrl &url, NavigationType, bool) override;
+};
+
+
+class LootDialog : public QDialog
+{
+ Q_OBJECT;
+public:
+ LootDialog(QWidget* parent, OrganizerCore& core, Loot& loot);
+ ~LootDialog();
+
+ void setText(const QString& s);
+ void setProgress(lootcli::Progress p);
+
+ void addOutput(const QString& s);
+ bool result() const;
+ void cancel();
+ void openReport();
+
+ int exec() override;
+ void accept() override;
+ void reject() override;
+
+private:
+ std::unique_ptr<Ui::LootDialog> ui;
+ MOBase::ExpanderWidget m_expander;
+ OrganizerCore& m_core;
+ Loot& m_loot;
+ bool m_finished;
+ bool m_cancelling;
+ MarkdownDocument m_report;
+
+ void createUI();
+ void closeEvent(QCloseEvent* e) override;
+ void addLineOutput(const QString& line);
+ void onFinished();
+ void log(MOBase::log::Levels lv, const QString& s);
+ void showReport();
+};
+
+#endif // MODORGANIZER_LOOTDIALOG_H
diff --git a/src/lootdialog.ui b/src/lootdialog.ui
new file mode 100644
index 00000000..e366ce41
--- /dev/null
+++ b/src/lootdialog.ui
@@ -0,0 +1,241 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>LootDialog</class>
+ <widget class="QDialog" name="LootDialog">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>457</width>
+ <height>600</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>LOOT</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <item>
+ <widget class="QWidget" name="widget" native="true">
+ <layout class="QVBoxLayout" name="verticalLayout_5" stretch="1,0">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QWidget" name="widget_2" native="true">
+ <layout class="QVBoxLayout" name="verticalLayout_2" stretch="0,0,1">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QLabel" name="progressText">
+ <property name="text">
+ <string>Progress</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QProgressBar" name="progressBar">
+ <property name="value">
+ <number>24</number>
+ </property>
+ <property name="textVisible">
+ <bool>false</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QFrame" name="frame">
+ <property name="frameShape">
+ <enum>QFrame::StyledPanel</enum>
+ </property>
+ <property name="frameShadow">
+ <enum>QFrame::Sunken</enum>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_6">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QWebEngineView" name="report">
+ <property name="url">
+ <url>
+ <string>about:blank</string>
+ </url>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QWidget" name="widget_3" native="true">
+ <layout class="QVBoxLayout" name="verticalLayout_4">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QToolButton" name="details">
+ <property name="text">
+ <string>Details</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QWidget" name="detailsPanel" native="true">
+ <layout class="QVBoxLayout" name="verticalLayout_3">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QWidget" name="widget_5" native="true">
+ <layout class="QHBoxLayout" name="horizontalLayout">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QPushButton" name="openJsonReport">
+ <property name="text">
+ <string>Open JSON report</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>40</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPlainTextEdit" name="output"/>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QDialogButtonBox" name="buttons">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="standardButtons">
+ <set>QDialogButtonBox::Close</set>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <customwidgets>
+ <customwidget>
+ <class>QWebEngineView</class>
+ <extends>QWidget</extends>
+ <header location="global">QtWebEngineWidgets/QWebEngineView</header>
+ </customwidget>
+ </customwidgets>
+ <resources/>
+ <connections>
+ <connection>
+ <sender>buttons</sender>
+ <signal>accepted()</signal>
+ <receiver>LootDialog</receiver>
+ <slot>accept()</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>248</x>
+ <y>254</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>157</x>
+ <y>274</y>
+ </hint>
+ </hints>
+ </connection>
+ <connection>
+ <sender>buttons</sender>
+ <signal>rejected()</signal>
+ <receiver>LootDialog</receiver>
+ <slot>reject()</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>316</x>
+ <y>260</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>286</x>
+ <y>274</y>
+ </hint>
+ </hints>
+ </connection>
+ </connections>
+</ui>
diff --git a/src/main.cpp b/src/main.cpp
index f2571685..29d2d02c 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -38,8 +38,7 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#include "executableslist.h"
#include "singleinstance.h"
#include "utility.h"
-#include "helper.h"
-#include "logbuffer.h"
+#include "loglist.h"
#include "selectiondialog.h"
#include "moapplication.h"
#include "tutorialmanager.h"
@@ -47,10 +46,13 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#include "instancemanager.h"
#include "moshortcut.h"
#include "organizercore.h"
+#include "env.h"
+#include "envmodule.h"
#include <eh.h>
#include <windows_error.h>
#include <usvfs.h>
+#include <log.h>
#include <QApplication>
#include <QPushButton>
@@ -59,7 +61,6 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#include <QCheckBox>
#include <QDir>
#include <QFileInfo>
-#include <QSettings>
#include <QWhatsThis>
#include <QToolBar>
#include <QFileDialog>
@@ -90,6 +91,11 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
using namespace MOBase;
using namespace MOShared;
+
+void sanityChecks(const env::Environment& env);
+int checkIncompatibleModule(const env::Module& m);
+int checkPathsForSanity(MOBase::IPluginGame& game, const Settings& s);
+
bool createAndMakeWritable(const std::wstring &subPath) {
QString const dataPath = qApp->property("dataPath").toString();
QString fullPath = dataPath + "/" + QString::fromStdWString(subPath);
@@ -113,14 +119,10 @@ bool bootstrap()
shellDelete(QStringList(backupDirectory));
}
- // cycle logfile
+ // cycle log file
removeOldFiles(qApp->property("dataPath").toString() + "/" + QString::fromStdWString(AppConfig::logPath()),
"usvfs*.log", 5, QDir::Name);
- if (!createAndMakeWritable(AppConfig::logPath())) {
- return false;
- }
-
return true;
}
@@ -132,9 +134,9 @@ static LONG WINAPI MyUnhandledExceptionFilter(struct _EXCEPTION_POINTERS *except
int dumpRes =
CreateMiniDump(exceptionPtrs, OrganizerCore::getGlobalCrashDumpsType(), dumpPath.c_str());
if (!dumpRes)
- qCritical("ModOrganizer has crashed, crash dump created.");
+ log::error("ModOrganizer has crashed, crash dump created.");
else
- qCritical("ModOrganizer has crashed, CreateMiniDump failed (%d, error %lu).", dumpRes, GetLastError());
+ log::error("ModOrganizer has crashed, CreateMiniDump failed ({}, error {}).", dumpRes, GetLastError());
if (prevUnhandledExceptionFilter)
return prevUnhandledExceptionFilter(exceptionPtrs);
@@ -246,68 +248,74 @@ static bool HaveWriteAccess(const std::wstring &path)
}
-QString determineProfile(QStringList &arguments, const QSettings &settings)
+QString determineProfile(QStringList &arguments, const Settings &settings)
{
- QString selectedProfileName = QString::fromUtf8(settings.value("selected_profile", "").toByteArray());
+ auto selectedProfileName = settings.game().selectedProfileName();
+
{ // see if there is a profile on the command line
int profileIndex = arguments.indexOf("-p", 1);
if ((profileIndex != -1) && (profileIndex < arguments.size() - 1)) {
- qDebug("profile overwritten on command line");
+ log::debug("profile overwritten on command line");
selectedProfileName = arguments.at(profileIndex + 1);
}
arguments.removeAt(profileIndex);
arguments.removeAt(profileIndex);
}
- if (selectedProfileName.isEmpty()) {
- qDebug("no configured profile");
+
+ if (!selectedProfileName) {
+ log::debug("no configured profile");
selectedProfileName = "Default";
} else {
- qDebug("configured profile: %s", qUtf8Printable(selectedProfileName));
+ log::debug("configured profile: {}", *selectedProfileName);
}
- return selectedProfileName;
+ return *selectedProfileName;
}
-MOBase::IPluginGame *selectGame(QSettings &settings, QDir const &gamePath, MOBase::IPluginGame *game)
+MOBase::IPluginGame *selectGame(
+ Settings &settings, QDir const &gamePath, MOBase::IPluginGame *game)
{
- settings.setValue("gameName", game->gameName());
- //Sadly, hookdll needs gamePath in order to run. So following code block is
- //commented out
- /*if (gamePath == game->gameDirectory()) {
- settings.remove("gamePath");
- } else*/ {
- QString gameDir = gamePath.absolutePath();
- game->setGamePath(gameDir);
- settings.setValue("gamePath", QDir::toNativeSeparators(gameDir).toUtf8().constData());
- }
- return game; //Woot
+ settings.game().setName(game->gameName());
+
+ QString gameDir = gamePath.absolutePath();
+ game->setGamePath(gameDir);
+
+ settings.game().setDirectory(gameDir);
+
+ return game;
}
-MOBase::IPluginGame *determineCurrentGame(QString const &moPath, QSettings &settings, PluginContainer const &plugins)
+MOBase::IPluginGame *determineCurrentGame(
+ QString const &moPath, Settings &settings, PluginContainer const &plugins)
{
//Determine what game we are running where. Be very paranoid in case the
//user has done something odd.
//If the game name has been set up, try to use that.
- QString gameName = settings.value("gameName", "").toString();
- bool gameConfigured = !gameName.isEmpty();
+ const auto gameName = settings.game().name();
+ const bool gameConfigured = (gameName.has_value() && *gameName != "");
+
if (gameConfigured) {
- MOBase::IPluginGame *game = plugins.managedGame(gameName);
+ MOBase::IPluginGame *game = plugins.managedGame(*gameName);
if (game == nullptr) {
- reportError(QObject::tr("Plugin to handle %1 no longer installed").arg(gameName));
+ reportError(QObject::tr("Plugin to handle %1 no longer installed").arg(*gameName));
return nullptr;
}
- QString gamePath = QString::fromUtf8(settings.value("gamePath", "").toByteArray());
- if (gamePath == "") {
+
+ auto gamePath = settings.game().directory();
+ if (!gamePath || *gamePath == "") {
gamePath = game->gameDirectory().absolutePath();
}
- QDir gameDir(gamePath);
+
+ QDir gameDir(*gamePath);
QFileInfo directoryInfo(gameDir.path());
+
if (directoryInfo.isSymLink()) {
reportError(QObject::tr("The configured path to the game directory (%1) appears to be a symbolic (or other) link. "
- "This setup is incompatible with MO2's VFS and will not run correctly.").arg(gamePath));
+ "This setup is incompatible with MO2's VFS and will not run correctly.").arg(*gamePath));
}
+
if (game->looksValid(gameDir)) {
return selectGame(settings, gameDir, game);
}
@@ -316,17 +324,20 @@ MOBase::IPluginGame *determineCurrentGame(QString const &moPath, QSettings &sett
//If we've made it this far and the instance is already configured for a game, something has gone wrong.
//Tell the user about it.
if (gameConfigured) {
- QString gamePath = QString::fromUtf8(settings.value("gamePath", "").toByteArray());
- reportError(QObject::tr("Could not use configuration settings for game \"%1\", path \"%2\".").
- arg(gameName).arg(gamePath));
+ const auto gamePath = settings.game().directory();
+
+ reportError(
+ QObject::tr("Could not use configuration settings for game \"%1\", path \"%2\".")
+ .arg(*gameName).arg(gamePath ? *gamePath : ""));
}
- SelectionDialog selection(gameConfigured ? QObject::tr("Please select the installation of %1 to manage").arg(gameName)
- : QObject::tr("Please select the game to manage"), nullptr, QSize(32, 32));
+ SelectionDialog selection(gameConfigured ?
+ QObject::tr("Please select the installation of %1 to manage").arg(*gameName) :
+ QObject::tr("Please select the game to manage"), nullptr, QSize(32, 32));
for (IPluginGame *game : plugins.plugins<IPluginGame>()) {
//If a game is already configured, skip any plugins that are not for that game
- if (gameConfigured && gameName.compare(game->gameName(), Qt::CaseInsensitive) != 0)
+ if (gameConfigured && gameName->compare(game->gameName(), Qt::CaseInsensitive) != 0)
continue;
//Only add games that are installed
@@ -350,9 +361,11 @@ MOBase::IPluginGame *determineCurrentGame(QString const &moPath, QSettings &sett
return selectGame(settings, game->gameDirectory(), game);
}
- gamePath = QFileDialog::getExistingDirectory(nullptr, gameConfigured ? QObject::tr("Please select the installation of %1 to manage").arg(gameName)
- : QObject::tr("Please select the game to manage"),
- QString(), QFileDialog::ShowDirsOnly);
+ gamePath = QFileDialog::getExistingDirectory(nullptr, gameConfigured ?
+ QObject::tr("Please select the installation of %1 to manage").arg(*gameName) :
+ QObject::tr("Please select the game to manage"),
+ QString(), QFileDialog::ShowDirsOnly);
+
if (!gamePath.isEmpty()) {
QDir gameDir(gamePath);
QFileInfo directoryInfo(gamePath);
@@ -363,7 +376,7 @@ MOBase::IPluginGame *determineCurrentGame(QString const &moPath, QSettings &sett
QList<IPluginGame *> possibleGames;
for (IPluginGame * const game : plugins.plugins<IPluginGame>()) {
//If a game is already configured, skip any plugins that are not for that game
- if (gameConfigured && gameName.compare(game->gameName(), Qt::CaseInsensitive) != 0)
+ if (gameConfigured && gameName->compare(game->gameName(), Qt::CaseInsensitive) != 0)
continue;
//Only try plugins that look valid for this directory
@@ -371,24 +384,31 @@ MOBase::IPluginGame *determineCurrentGame(QString const &moPath, QSettings &sett
possibleGames.append(game);
}
}
+
if (possibleGames.count() > 1) {
- SelectionDialog browseSelection(gameConfigured ? QObject::tr("Please select the installation of %1 to manage").arg(gameName)
- : QObject::tr("Please select the game to manage"),
- nullptr, QSize(32, 32));
+ SelectionDialog browseSelection(gameConfigured ?
+ QObject::tr("Please select the installation of %1 to manage").arg(*gameName) :
+ QObject::tr("Please select the game to manage"),
+ nullptr, QSize(32, 32));
+
for (IPluginGame *game : possibleGames) {
browseSelection.addChoice(game->gameIcon(), game->gameName(), gamePath, QVariant::fromValue(game));
}
+
if (browseSelection.exec() == QDialog::Accepted) {
return selectGame(settings, gameDir, browseSelection.getChoiceData().value<IPluginGame *>());
} else {
- reportError(gameConfigured ? QObject::tr("Canceled finding %1 in \"%2\".").arg(gameName).arg(gamePath)
- : QObject::tr("Canceled finding game in \"%1\".").arg(gamePath));
+ reportError(gameConfigured ?
+ QObject::tr("Canceled finding %1 in \"%2\".").arg(*gameName).arg(gamePath) :
+ QObject::tr("Canceled finding game in \"%1\".").arg(gamePath));
}
} else if(possibleGames.count() == 1) {
return selectGame(settings, gameDir, possibleGames[0]);
} else {
if (gameConfigured) {
- reportError(QObject::tr("%1 not identified in \"%2\". The directory is required to contain the game binary.").arg(gameName).arg(gamePath));
+ reportError(
+ QObject::tr("%1 not identified in \"%2\". The directory is required to contain the game binary.")
+ .arg(*gameName).arg(gamePath));
} else {
QString supportedGames;
@@ -424,9 +444,6 @@ void setupPath()
{
static const int BUFSIZE = 4096;
- qDebug("MO at: %s", qUtf8Printable(QDir::toNativeSeparators(
- QCoreApplication::applicationDirPath())));
-
QCoreApplication::setLibraryPaths(QStringList(QCoreApplication::applicationDirPath() + "/dlls") + QCoreApplication::libraryPaths());
boost::scoped_array<TCHAR> oldPath(new TCHAR[BUFSIZE]);
@@ -446,8 +463,6 @@ void setupPath()
void preloadDll(const QString& filename)
{
- qDebug().nospace() << "preloading " << filename;
-
if (GetModuleHandleW(filename.toStdWString().c_str())) {
// already loaded, this can happen when "restarting" MO by switching
// instances, for example
@@ -460,16 +475,13 @@ void preloadDll(const QString& filename)
const auto dllPath = appPath + "\\" + filename;
if (!QFile::exists(dllPath)) {
- qWarning().nospace() << dllPath << "not found";
+ log::warn("{} not found", dllPath);
return;
}
if (!LoadLibraryW(dllPath.toStdWString().c_str())) {
const auto e = GetLastError();
-
- qWarning().nospace()
- << "failed to load " << dllPath << ": "
- << formatSystemMessage(e);
+ log::warn("failed to load {}: {}", dllPath, formatSystemMessage(e));
}
}
@@ -489,93 +501,101 @@ static QString getVersionDisplayString()
return createVersionInfo().displayString(3);
}
+
int runApplication(MOApplication &application, SingleInstance &instance,
const QString &splashPath)
{
+ log::info(
+ "starting Mod Organizer version {} revision {} in {}, usvfs: {}",
+ getVersionDisplayString(), GITID, QCoreApplication::applicationDirPath(),
+ MOShared::getUsvfsVersionString());
- qDebug().nospace()
- << "Starting Mod Organizer version "
- << getVersionDisplayString() << " revision " << GITID;
-
-#if !defined(QT_NO_SSL)
preloadSsl();
- qDebug("ssl support: %d", QSslSocket::supportsSsl());
-#else
- qDebug("non-ssl build");
-#endif
-
- {
- env::Environment env;
-
- qDebug().nospace().noquote()
- << "windows: " << env.windowsInfo().toString();
-
- if (env.windowsInfo().compatibilityMode()) {
- qWarning() << "MO seems to be running in compatibility mode";
- }
+ if (!QSslSocket::supportsSsl()) {
+ log::warn("no ssl support");
+ }
- qDebug().nospace().noquote() << "security products:";
- for (const auto& sp : env.securityProducts()) {
- qDebug().nospace().noquote() << " . " << sp.toString();
- }
+ const QString dataPath = application.property("dataPath").toString();
+ log::info("data path: {}", dataPath);
- qDebug() << "modules loaded in process:";
- for (const auto& m : env.loadedModules()) {
- qDebug().nospace().noquote() << " . " << m.toString();
- }
+ if (InstanceManager::isPortablePath(dataPath)) {
+ log::debug("this is a portable instance");
}
- QString dataPath = application.property("dataPath").toString();
- qDebug("data path: %s", qUtf8Printable(dataPath));
-
if (!bootstrap()) {
reportError("failed to set up data paths");
return 1;
}
- QWindowsWindowFunctions::setWindowActivationBehavior(QWindowsWindowFunctions::AlwaysActivateWindow);
+ QWindowsWindowFunctions::setWindowActivationBehavior(
+ QWindowsWindowFunctions::AlwaysActivateWindow);
QStringList arguments = application.arguments();
try {
- qDebug("Working directory: %s", qUtf8Printable(QDir::toNativeSeparators(QDir::currentPath())));
+ log::info("working directory: {}", QDir::currentPath());
- QSettings settings(dataPath + "/"
- + QString::fromStdWString(AppConfig::iniFileName()),
- QSettings::IniFormat);
+ Settings settings(dataPath + "/" + QString::fromStdWString(AppConfig::iniFileName()));
+ log::getDefault().setLevel(settings.diagnostics().logLevel());
- // global crashDumpType sits in OrganizerCore to make a bit less ugly to update it when the settings are changed during runtime
- OrganizerCore::setGlobalCrashDumpsType(settings.value("Settings/crash_dumps_type", static_cast<int>(CrashDumpsType::Mini)).toInt());
+ log::debug("using ini at '{}'", settings.filename());
- qDebug("Loaded settings:");
- settings.beginGroup("Settings");
- for (auto k : settings.allKeys())
- if (!k.contains("username") && !k.contains("password") && !k.contains("nexus_api_key"))
- qDebug(" %s=%s", k.toUtf8().data(), settings.value(k).toString().toUtf8().data());
- settings.endGroup();
+ // global crashDumpType sits in OrganizerCore to make a bit less ugly to
+ // update it when the settings are changed during runtime
+ OrganizerCore::setGlobalCrashDumpsType(settings.diagnostics().crashDumpsType());
+ env::Environment env;
+ env.dump(settings);
+ settings.dump();
+ sanityChecks(env);
+
+ const auto moduleNotification = env.onModuleLoaded(qApp, [](auto&& m) {
+ log::debug("loaded module {}", m.toString());
+ checkIncompatibleModule(m);
+ });
- qDebug("initializing core");
+ log::debug("initializing core");
OrganizerCore organizer(settings);
if (!organizer.bootstrap()) {
reportError("failed to set up data paths");
return 1;
}
- qDebug("initialize plugins");
+
+ {
+ // log if there are any dmp files
+ const auto hasCrashDumps =
+ !QDir(QString::fromStdWString(organizer.crashDumpsPath()))
+ .entryList({"*.dmp"}, QDir::Files)
+ .empty();
+
+ if (hasCrashDumps) {
+ log::debug(
+ "there are crash dumps in '{}'",
+ QString::fromStdWString(organizer.crashDumpsPath()));
+ }
+ }
+
+ log::debug("initializing plugins");
PluginContainer pluginContainer(&organizer);
pluginContainer.loadPlugins();
MOBase::IPluginGame *game = determineCurrentGame(
application.applicationDirPath(), settings, pluginContainer);
+
if (game == nullptr) {
InstanceManager &instance = InstanceManager::instance();
QString instanceName = instance.currentInstance();
+
if (instanceName.compare("Portable", Qt::CaseInsensitive) != 0) {
instance.clearCurrentInstance();
- return INT_MAX;
+ return RestartExitCode;
}
+
return 1;
}
+
+ checkPathsForSanity(*game, settings);
+
if (splashPath.startsWith(':')) {
// currently using MO splash, see if the plugin contains one
QString pluginSplash
@@ -583,15 +603,17 @@ int runApplication(MOApplication &application, SingleInstance &instance,
QImage image(pluginSplash);
if (!image.isNull()) {
image.save(dataPath + "/splash.png");
- } else {
- qDebug("no plugin splash");
}
}
organizer.setManagedGame(game);
organizer.createDefaultProfile();
- if (!settings.contains("game_edition")) {
+ QString edition;
+
+ if (auto v=settings.game().edition()) {
+ edition = *v;
+ } else {
QStringList editions = game->gameVariants();
if (editions.size() > 1) {
SelectionDialog selection(
@@ -607,129 +629,141 @@ int runApplication(MOApplication &application, SingleInstance &instance,
if (selection.exec() == QDialog::Rejected) {
return 1;
} else {
- settings.setValue("game_edition", selection.getChoiceString());
+ edition = selection.getChoiceString();
+ settings.game().setEdition(edition);
}
}
}
- game->setGameVariant(settings.value("game_edition").toString());
- qDebug("managing game at %s", qUtf8Printable(QDir::toNativeSeparators(
- game->gameDirectory().absolutePath())));
+ Q_ASSERT(!edition.isEmpty());
+
+ game->setGameVariant(edition);
- organizer.updateExecutablesList(settings);
+ log::info(
+ "using game plugin '{}' ('{}', steam id '{}') at {}",
+ game->gameName(), game->gameShortName(), game->steamAPPId(),
+ game->gameDirectory().absolutePath());
+
+ organizer.updateExecutablesList();
QString selectedProfileName = determineProfile(arguments, settings);
organizer.setCurrentProfile(selectedProfileName);
// if we have a command line parameter, it is either a nxm link or
// a binary to start
- if (arguments.size() > 1) {
- if (MOShortcut shortcut{ arguments.at(1) }) {
- if (shortcut.hasExecutable()) {
- try {
- organizer.runShortcut(shortcut);
- return 0;
- }
- catch (const std::exception &e) {
- reportError(
- QObject::tr("failed to start shortcut: %1").arg(e.what()));
- return 1;
- }
- }
- }
- else if (OrganizerCore::isNxmLink(arguments.at(1))) {
- qDebug("starting download from command line: %s",
- qUtf8Printable(arguments.at(1)));
- organizer.externalMessage(arguments.at(1));
- }
- else {
- QString exeName = arguments.at(1);
- qDebug("starting %s from command line", qUtf8Printable(exeName));
- arguments.removeFirst(); // remove application name (ModOrganizer.exe)
- arguments.removeFirst(); // remove binary name
- // pass the remaining parameters to the binary
- try {
- organizer.startApplication(exeName, arguments, QString(), QString());
- return 0;
- }
- catch (const std::exception &e) {
- reportError(
- QObject::tr("failed to start application: %1").arg(e.what()));
- return 1;
- }
- }
- }
+ if (arguments.size() > 1) {
+ if (MOShortcut shortcut{ arguments.at(1) }) {
+ if (shortcut.hasExecutable()) {
+ try {
+ organizer.processRunner()
+ .setFromShortcut(shortcut)
+ .setWaitForCompletion()
+ .run();
- QPixmap pixmap(splashPath);
- QSplashScreen splash(pixmap);
+ return 0;
+ }
+ catch (const std::exception &e) {
+ reportError(
+ QObject::tr("failed to start shortcut: %1").arg(e.what()));
+ return 1;
+ }
+ }
+ }
+ else if (OrganizerCore::isNxmLink(arguments.at(1))) {
+ log::debug("starting download from command line: {}", arguments.at(1));
+ organizer.externalMessage(arguments.at(1));
+ }
+ else {
+ QString exeName = arguments.at(1);
+ log::debug("starting {} from command line", exeName);
- if (settings.contains("window_monitor")) {
- const int monitor = settings.value("window_monitor").toInt();
+ arguments.removeFirst(); // remove application name (ModOrganizer.exe)
+ arguments.removeFirst(); // remove binary name
- if (monitor != -1) {
- QDesktopWidget* desktop = QApplication::desktop();
- const QPoint center = desktop->availableGeometry(monitor).center();
- splash.move(center - splash.rect().center());
- }
- }
+ try
+ {
+ // pass the remaining parameters to the binary
+ organizer.processRunner()
+ .setFromFileOrExecutable(exeName, arguments)
+ .setWaitForCompletion()
+ .run();
+ return 0;
+ }
+ catch (const std::exception &e)
+ {
+ reportError(
+ QObject::tr("failed to start application: %1").arg(e.what()));
+ return 1;
+ }
+ }
+ }
+
+ QPixmap pixmap(splashPath);
+ QSplashScreen splash(pixmap);
+ settings.geometry().centerOnMainWindowMonitor(&splash);
splash.show();
splash.activateWindow();
QString apiKey;
- if (organizer.settings().getNexusApiKey(apiKey)) {
+ if (settings.nexus().apiKey(apiKey)) {
NexusInterface::instance(&pluginContainer)->getAccessManager()->apiCheck(apiKey);
}
- qDebug("initializing tutorials");
+ log::debug("initializing tutorials");
TutorialManager::init(
qApp->applicationDirPath() + "/"
+ QString::fromStdWString(AppConfig::tutorialsPath()) + "/",
&organizer);
- if (!application.setStyleFile(settings.value("Settings/style", "").toString())) {
+ if (!application.setStyleFile(settings.interface().styleName().value_or(""))) {
// disable invalid stylesheet
- settings.setValue("Settings/style", "");
+ settings.interface().setStyleName("");
}
int res = 1;
+
{ // scope to control lifetime of mainwindow
// set up main window and its data structures
MainWindow mainWindow(settings, organizer, pluginContainer);
+ NexusInterface::instance(&pluginContainer)
+ ->getAccessManager()->setTopLevelWidget(&mainWindow);
+
QObject::connect(&mainWindow, SIGNAL(styleChanged(QString)), &application,
SLOT(setStyleFile(QString)));
QObject::connect(&instance, SIGNAL(messageSent(QString)), &organizer,
SLOT(externalMessage(QString)));
- mainWindow.processUpdates();
- mainWindow.readSettings();
+ // this must be before readSettings(), see DockFixer in mainwindow.cpp
+ splash.finish(&mainWindow);
- qDebug("displaying main window");
+ log::debug("displaying main window");
mainWindow.show();
mainWindow.activateWindow();
splash.finish(&mainWindow);
- return application.exec();
+
+ res = application.exec();
+ mainWindow.close();
+
+ NexusInterface::instance(&pluginContainer)
+ ->getAccessManager()->setTopLevelWidget(nullptr);
}
+
+ settings.geometry().resetIfNeeded();
+ return res;
+
} catch (const std::exception &e) {
reportError(e.what());
- return 1;
}
+
+ return 1;
}
int doCoreDump(env::CoreDumpTypes type)
{
- // open a console
- AllocConsole();
-
- // redirect stdin, stdout and stderr to it
- FILE* in=nullptr;
- FILE* out=nullptr;
- FILE* err=nullptr;
- freopen_s(&in, "CONIN$", "r", stdin);
- freopen_s(&out, "CONOUT$", "w", stdout);
- freopen_s(&err, "CONOUT$", "w", stderr);
+ env::Console c;
// dump
const auto b = env::coredumpOther(type);
@@ -740,17 +774,74 @@ int doCoreDump(env::CoreDumpTypes type)
std::wcerr << L"Press enter to continue...";
std::wcin.get();
- // close redirected handles
- std::fclose(err);
- std::fclose(out);
- std::fclose(in);
+ return (b ? 0 : 1);
+}
- // close console
- FreeConsole();
+log::Levels convertQtLevel(QtMsgType t)
+{
+ switch (t)
+ {
+ case QtDebugMsg:
+ return log::Debug;
- return (b ? 0 : 1);
+ case QtWarningMsg:
+ return log::Warning;
+
+ case QtCriticalMsg: // fall-through
+ case QtFatalMsg:
+ return log::Error;
+
+ case QtInfoMsg: // fall-through
+ default:
+ return log::Info;
+ }
}
+void qtLogCallback(
+ QtMsgType type, const QMessageLogContext& context, const QString& message)
+{
+ std::string_view file = "";
+
+ if (type != QtDebugMsg) {
+ if (context.file) {
+ file = context.file;
+
+ const auto lastSep = file.find_last_of("/\\");
+ if (lastSep != std::string_view::npos) {
+ file = {context.file + lastSep + 1};
+ }
+ }
+ }
+
+ if (file.empty()) {
+ log::log(
+ convertQtLevel(type), "{}",
+ message.toStdString());
+ } else {
+ log::log(
+ convertQtLevel(type), "[{}:{}] {}",
+ file, context.line, message.toStdString());
+ }
+}
+
+void initLogging()
+{
+ LogModel::create();
+
+ log::LoggerConfiguration conf;
+ conf.maxLevel = MOBase::log::Debug;
+ conf.pattern = "%^[%Y-%m-%d %H:%M:%S.%e %L] %v%$";
+ conf.utc = true;
+
+ log::createDefault(conf);
+
+ log::getDefault().setCallback(
+ [](log::Entry e){ LogModel::instance().add(e); });
+
+ qInstallMessageHandler(qtLogCallback);
+}
+
+
int main(int argc, char *argv[])
{
// handle --crashdump first
@@ -764,6 +855,8 @@ int main(int argc, char *argv[])
}
}
+ initLogging();
+
//Make sure the configured temp folder exists
QDir tempDir = QDir::temp();
if (!tempDir.exists())
@@ -797,7 +890,7 @@ int main(int argc, char *argv[])
if (moshortcut ||
arguments.size() > 1 && OrganizerCore::isNxmLink(arguments.at(1)))
{
- qDebug("not primary instance, sending shortcut/download message");
+ log::debug("not primary instance, sending shortcut/download message");
instance.sendMessage(arguments.at(1));
return 0;
} else if (arguments.size() == 1) {
@@ -809,6 +902,13 @@ int main(int argc, char *argv[])
} // we continue for the primary instance OR if MO was called with parameters
do {
+ LogModel::instance().clear();
+ ResetExitFlag();
+
+ // make sure the log file isn't locked in case MO was restarted and
+ // the previous instance gets deleted
+ log::getDefault().setFile({});
+
QString dataPath;
try {
@@ -826,17 +926,26 @@ int main(int argc, char *argv[])
// initialize dump collection only after "dataPath" since the crashes are stored under it
prevUnhandledExceptionFilter = SetUnhandledExceptionFilter(MyUnhandledExceptionFilter);
- LogBuffer::init(1000000, QtDebugMsg, qApp->property("dataPath").toString() + "/logs/mo_interface.log");
+ const auto logFile =
+ qApp->property("dataPath").toString() + "/logs/mo_interface.log";
+
+ if (!createAndMakeWritable(AppConfig::logPath())) {
+ reportError("Failed to create log folder");
+ return 1;
+ }
+
+ log::getDefault().setFile(MOBase::log::File::single(logFile.toStdWString()));
QString splash = dataPath + "/splash.png";
if (!QFile::exists(dataPath + "/splash.png")) {
splash = ":/MO/gui/splash";
}
- int result = runApplication(application, instance, splash);
- if (result != INT_MAX) {
+ const int result = runApplication(application, instance, splash);
+ if (result != RestartExitCode) {
return result;
}
+
argc = 1;
moshortcut = MOShortcut("");
} while (true);
diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp
index 451ec688..5da42b93 100644
--- a/src/mainwindow.cpp
+++ b/src/mainwindow.cpp
@@ -39,7 +39,6 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#include "spawn.h"
#include "versioninfo.h"
#include "instancemanager.h"
-
#include "report.h"
#include "modlist.h"
#include "modlistsortproxy.h"
@@ -57,14 +56,12 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#include "downloadlistwidget.h"
#include "messagedialog.h"
#include "installationmanager.h"
-#include "lockeddialog.h"
-#include "waitingonclosedialog.h"
-#include "logbuffer.h"
#include "downloadlistsortproxy.h"
#include "motddialog.h"
#include "filedialogmemory.h"
#include "tutorialmanager.h"
#include "modflagicondelegate.h"
+#include "modconflicticondelegate.h"
#include "genericicondelegate.h"
#include "selectiondialog.h"
#include "csvbuilder.h"
@@ -73,11 +70,13 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#include "previewdialog.h"
#include "browserdialog.h"
#include "aboutdialog.h"
+#include "settingsdialog.h"
#include <safewritefile.h>
#include "nxmaccessmanager.h"
#include "appconfig.h"
#include "eventfilter.h"
#include "statusbar.h"
+#include "filterlist.h"
#include <utility.h>
#include <dataarchives.h>
#include <bsainvalidation.h>
@@ -86,6 +85,7 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#include <usvfs.h>
#include "localsavegames.h"
#include "listdialog.h"
+#include "envshortcut.h"
#include <QAbstractItemDelegate>
#include <QAbstractProxyModel>
@@ -99,7 +99,6 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#include <QCoreApplication>
#include <QCursor>
#include <QDebug>
-#include <QDesktopWidget>
#include <QDesktopServices>
#include <QDialog>
#include <QDirIterator>
@@ -138,7 +137,6 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#include <QRect>
#include <QRegExp>
#include <QResizeEvent>
-#include <QSettings>
#include <QScopedPointer>
#include <QSizePolicy>
#include <QSize>
@@ -194,20 +192,24 @@ const QSize SmallToolbarSize(24, 24);
const QSize MediumToolbarSize(32, 32);
const QSize LargeToolbarSize(42, 36);
+QString UnmanagedModName()
+{
+ return QObject::tr("<Unmanaged>");
+}
+
+bool runLoot(QWidget* parent, OrganizerCore& core, bool didUpdateMasterList);
-MainWindow::MainWindow(QSettings &initSettings
+MainWindow::MainWindow(Settings &settings
, OrganizerCore &organizerCore
, PluginContainer &pluginContainer
, QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
, m_WasVisible(false)
- , m_menuBarVisible(true)
- , m_statusBarVisible(true)
+ , m_FirstPaint(true)
, m_linksSeparator(nullptr)
, m_Tutorial(this, "MainWindow")
, m_OldProfileIndex(-1)
- , m_ModListGroupingProxy(nullptr)
, m_ModListSortProxy(nullptr)
, m_OldExecutableIndex(-1)
, m_CategoryFactory(CategoryFactory::instance())
@@ -226,11 +228,12 @@ MainWindow::MainWindow(QSettings &initSettings
{
QWebEngineProfile::defaultProfile()->setPersistentCookiesPolicy(QWebEngineProfile::NoPersistentCookies);
QWebEngineProfile::defaultProfile()->setHttpCacheMaximumSize(52428800);
- QWebEngineProfile::defaultProfile()->setCachePath(m_OrganizerCore.settings().getCacheDirectory());
- QWebEngineProfile::defaultProfile()->setPersistentStoragePath(m_OrganizerCore.settings().getCacheDirectory());
+ QWebEngineProfile::defaultProfile()->setCachePath(settings.paths().cache());
+ QWebEngineProfile::defaultProfile()->setPersistentStoragePath(settings.paths().cache());
ui->setupUi(this);
- m_statusBar.reset(new StatusBar(statusBar(), ui));
+ languageChange(settings.interface().language());
+ ui->statusBar->setup(ui, settings);
{
auto* ni = NexusInterface::instance(&m_PluginContainer);
@@ -254,67 +257,31 @@ MainWindow::MainWindow(QSettings &initSettings
// in the rare case where the user restarts MO through the settings, this
// will correctly pick up the previous values
updateWindowTitle(ni->getAPIUserAccount());
- m_statusBar->setAPI(ni->getAPIStats(), ni->getAPIUserAccount());
+ ui->statusBar->setAPI(ni->getAPIStats(), ni->getAPIUserAccount());
}
- languageChange(m_OrganizerCore.settings().language());
-
m_CategoryFactory.loadCategories();
+ m_Filters.reset(new FilterList(ui, m_CategoryFactory));
+
+ connect(
+ m_Filters.get(), &FilterList::criteriaChanged,
+ [&](auto&& v) { onFiltersCriteria(v); });
+
+ connect(
+ m_Filters.get(), &FilterList::optionsChanged,
+ [&](auto&& mode, auto&& sep) { onFiltersOptions(mode, sep); });
- ui->logList->setModel(LogBuffer::instance());
- ui->logList->setColumnWidth(0, 100);
- ui->logList->setAutoScroll(true);
- ui->logList->scrollToBottom();
- ui->logList->addAction(ui->actionCopy_Log_to_Clipboard);
- int splitterSize = this->size().height(); // actually total window size, but the splitter doesn't seem to return the true value
- ui->topLevelSplitter->setSizes(QList<int>() << splitterSize - 100 << 100);
- connect(ui->logList->model(), SIGNAL(rowsInserted(const QModelIndex &, int, int)),
- ui->logList, SLOT(scrollToBottom()));
- connect(ui->logList->model(), SIGNAL(dataChanged(QModelIndex,QModelIndex)),
- ui->logList, SLOT(scrollToBottom()));
+ ui->logList->setCore(m_OrganizerCore);
updateProblemsButton();
setupToolbar();
toggleMO2EndorseState();
+ toggleUpdateAction();
TaskProgressManager::instance().tryCreateTaskbar();
- // set up mod list
- m_ModListSortProxy = m_OrganizerCore.createModListProxyModel();
-
- ui->modList->setModel(m_ModListSortProxy);
-
- GenericIconDelegate *contentDelegate = new GenericIconDelegate(ui->modList, Qt::UserRole + 3, ModList::COL_CONTENT, 150);
- connect(ui->modList->header(), SIGNAL(sectionResized(int,int,int)), contentDelegate, SLOT(columnResized(int,int,int)));
- ui->modList->sortByColumn(ModList::COL_PRIORITY, Qt::AscendingOrder);
- ModFlagIconDelegate *flagDelegate = new ModFlagIconDelegate(ui->modList, ModList::COL_FLAGS, 120);
- connect(ui->modList->header(), SIGNAL(sectionResized(int,int,int)), flagDelegate, SLOT(columnResized(int,int,int)));
- ui->modList->setItemDelegateForColumn(ModList::COL_FLAGS, flagDelegate);
- ui->modList->setItemDelegateForColumn(ModList::COL_CONTENT, contentDelegate);
- ui->modList->header()->installEventFilter(m_OrganizerCore.modList());
- connect(ui->modList->header(), SIGNAL(sectionResized(int, int, int)), this, SLOT(modListSectionResized(int, int, int)));
-
- bool modListAdjusted = registerWidgetState(ui->modList->objectName(), ui->modList->header(), "mod_list_state");
-
- if (modListAdjusted) {
- // hack: force the resize-signal to be triggered because restoreState doesn't seem to do that
- for (int column = 0; column <= ModList::COL_LASTCOLUMN; ++column) {
- int sectionSize = ui->modList->header()->sectionSize(column);
- ui->modList->header()->resizeSection(column, sectionSize + 1);
- ui->modList->header()->resizeSection(column, sectionSize);
- }
- } else {
- // hide these columns by default
- ui->modList->header()->setSectionHidden(ModList::COL_CONTENT, true);
- ui->modList->header()->setSectionHidden(ModList::COL_MODID, true);
- ui->modList->header()->setSectionHidden(ModList::COL_GAME, true);
- ui->modList->header()->setSectionHidden(ModList::COL_INSTALLTIME, true);
- ui->modList->header()->setSectionHidden(ModList::COL_NOTES, true);
- }
-
- ui->modList->header()->setSectionHidden(ModList::COL_NAME, false); // prevent the name-column from being hidden
- ui->modList->installEventFilter(m_OrganizerCore.modList());
+ setupModList();
// set up plugin list
m_PluginListSortProxy = m_OrganizerCore.createPluginListProxyModel();
@@ -325,17 +292,20 @@ MainWindow::MainWindow(QSettings &initSettings
ui->espList->installEventFilter(m_OrganizerCore.pluginList());
ui->bsaList->setLocalMoveOnly(true);
+ ui->bsaList->setHeaderHidden(true);
initDownloadView();
- bool pluginListAdjusted = registerWidgetState(ui->espList->objectName(), ui->espList->header(), "plugin_list_state");
- registerWidgetState(ui->dataTree->objectName(), ui->dataTree->header());
- registerWidgetState(ui->downloadView->objectName(),
- ui->downloadView->header());
+
+ const bool pluginListAdjusted =
+ settings.geometry().restoreState(ui->espList->header());
+
+ settings.geometry().restoreState(ui->dataTree->header());
+ settings.geometry().restoreState(ui->downloadView->header());
ui->splitter->setStretchFactor(0, 3);
ui->splitter->setStretchFactor(1, 2);
- resizeLists(modListAdjusted, pluginListAdjusted);
+ resizeLists(pluginListAdjusted);
QMenu *linkMenu = new QMenu(this);
m_LinkToolbar = linkMenu->addAction(QIcon(":/MO/gui/link"), tr("Toolbar and Menu"), this, SLOT(linkToolbar()));
@@ -372,8 +342,6 @@ MainWindow::MainWindow(QSettings &initSettings
connect(ui->savegameList, SIGNAL(itemEntered(QListWidgetItem*)), this, SLOT(saveSelectionChanged(QListWidgetItem*)));
- connect(ui->modList, SIGNAL(dropModeUpdate(bool)), m_OrganizerCore.modList(), SLOT(dropModeUpdate(bool)));
-
connect(m_ModListSortProxy, SIGNAL(filterActive(bool)), this, SLOT(modFilterActive(bool)));
connect(m_ModListSortProxy, SIGNAL(layoutChanged()), this, SLOT(updateModCount()));
connect(ui->modFilterEdit, SIGNAL(textChanged(QString)), m_ModListSortProxy, SLOT(updateFilter(QString)));
@@ -382,6 +350,7 @@ MainWindow::MainWindow(QSettings &initSettings
connect(ui->espFilterEdit, SIGNAL(textChanged(QString)), this, SLOT(espFilterChanged(QString)));
connect(ui->dataTree, SIGNAL(itemExpanded(QTreeWidgetItem*)), this, SLOT(expandDataTreeItem(QTreeWidgetItem*)));
+ connect(ui->dataTree, SIGNAL(itemActivated(QTreeWidgetItem*, int)), this, SLOT(activateDataTreeItem(QTreeWidgetItem*, int)));
connect(m_OrganizerCore.directoryRefresher(), SIGNAL(refreshed()), this, SLOT(directory_refreshed()));
connect(m_OrganizerCore.directoryRefresher(), SIGNAL(progress(int)), this, SLOT(refresher_progress(int)));
@@ -399,7 +368,6 @@ MainWindow::MainWindow(QSettings &initSettings
connect(NexusInterface::instance(&pluginContainer), SIGNAL(requestNXMDownload(QString)), &m_OrganizerCore, SLOT(downloadRequestedNXM(QString)));
connect(NexusInterface::instance(&pluginContainer), SIGNAL(nxmDownloadURLsAvailable(QString,int,int,QVariant,QVariant,int)), this, SLOT(nxmDownloadURLs(QString,int,int,QVariant,QVariant,int)));
connect(NexusInterface::instance(&pluginContainer), SIGNAL(needLogin()), &m_OrganizerCore, SLOT(nexusApi()));
- connect(NexusInterface::instance(&pluginContainer)->getAccessManager(), SIGNAL(validateFailed(QString)), this, SLOT(validationFailed(QString)));
connect(
NexusInterface::instance(&pluginContainer)->getAccessManager(),
@@ -421,9 +389,9 @@ MainWindow::MainWindow(QSettings &initSettings
connect(&TutorialManager::instance(), SIGNAL(windowTutorialFinished(QString)), this, SLOT(windowTutorialFinished(QString)));
connect(ui->tabWidget, SIGNAL(currentChanged(int)), &TutorialManager::instance(), SIGNAL(tabChanged(int)));
- connect(ui->modList->header(), SIGNAL(sortIndicatorChanged(int,Qt::SortOrder)), this, SLOT(modListSortIndicatorChanged(int,Qt::SortOrder)));
connect(ui->toolBar, SIGNAL(customContextMenuRequested(QPoint)), this, SLOT(toolBar_customContextMenuRequested(QPoint)));
- connect(ui->menuToolbars, &QMenu::aboutToShow, [&]{ toolbarMenu_aboutToShow(); });
+ connect(ui->menuToolbars, &QMenu::aboutToShow, [&]{ updateToolbarMenu(); });
+ connect(ui->menuView, &QMenu::aboutToShow, [&]{ updateViewMenu(); });
connect(&m_OrganizerCore, &OrganizerCore::modInstalled, this, &MainWindow::modInstalled);
connect(&m_OrganizerCore, &OrganizerCore::close, this, &QMainWindow::close);
@@ -434,7 +402,6 @@ MainWindow::MainWindow(QSettings &initSettings
connect(&m_CheckBSATimer, SIGNAL(timeout()), this, SLOT(checkBSAList()));
connect(ui->espList->selectionModel(), SIGNAL(selectionChanged(QItemSelection, QItemSelection)), this, SLOT(esplistSelectionsChanged(QItemSelection)));
- connect(ui->modList->selectionModel(), SIGNAL(selectionChanged(QItemSelection, QItemSelection)), this, SLOT(modlistSelectionsChanged(QItemSelection)));
new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_Enter), this, SLOT(openExplorer_activated()));
new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_Return), this, SLOT(openExplorer_activated()));
@@ -451,8 +418,7 @@ MainWindow::MainWindow(QSettings &initSettings
connect(&m_SaveMetaTimer, SIGNAL(timeout()), this, SLOT(saveModMetas()));
m_SaveMetaTimer.start(5000);
- setCategoryListVisible(initSettings.value("categorylist_visible", true).toBool());
- FileDialogMemory::restore(initSettings);
+ FileDialogMemory::restore(settings);
fixCategories();
@@ -461,7 +427,7 @@ MainWindow::MainWindow(QSettings &initSettings
m_Tutorial.expose("modList", m_OrganizerCore.modList());
m_Tutorial.expose("espList", m_OrganizerCore.pluginList());
- m_OrganizerCore.setUserInterface(this, this);
+ m_OrganizerCore.setUserInterface(this);
for (const QString &fileName : m_PluginContainer.pluginFileNames()) {
installTranslator(QFileInfo(fileName).baseName());
}
@@ -495,6 +461,90 @@ MainWindow::MainWindow(QSettings &initSettings
resetActionIcons();
updatePluginCount();
updateModCount();
+ processUpdates();
+}
+
+void MainWindow::setupModList()
+{
+ m_ModListSortProxy = m_OrganizerCore.createModListProxyModel();
+ ui->modList->setModel(m_ModListSortProxy);
+ ui->modList->sortByColumn(ModList::COL_PRIORITY, Qt::AscendingOrder);
+
+
+ connect(
+ ui->modList, SIGNAL(dropModeUpdate(bool)),
+ m_OrganizerCore.modList(), SLOT(dropModeUpdate(bool)));
+
+ connect(
+ ui->modList->header(), SIGNAL(sortIndicatorChanged(int,Qt::SortOrder)),
+ this, SLOT(modListSortIndicatorChanged(int,Qt::SortOrder)));
+
+ connect(
+ ui->modList->selectionModel(), SIGNAL(selectionChanged(QItemSelection, QItemSelection)),
+ this, SLOT(modlistSelectionsChanged(QItemSelection)));
+
+ connect(
+ ui->modList->header(), SIGNAL(sectionResized(int, int, int)),
+ this, SLOT(modListSectionResized(int, int, int)));
+
+
+ GenericIconDelegate *contentDelegate = new GenericIconDelegate(
+ ui->modList, Qt::UserRole + 3, ModList::COL_CONTENT, 150);
+
+ connect(
+ ui->modList->header(), SIGNAL(sectionResized(int,int,int)),
+ contentDelegate, SLOT(columnResized(int,int,int)));
+
+
+ ModFlagIconDelegate *flagDelegate = new ModFlagIconDelegate(
+ ui->modList, ModList::COL_FLAGS, 120);
+
+ connect(
+ ui->modList->header(), SIGNAL(sectionResized(int,int,int)),
+ flagDelegate, SLOT(columnResized(int,int,int)));
+
+
+ ModConflictIconDelegate* conflictFlagDelegate = new ModConflictIconDelegate(
+ ui->modList, ModList::COL_CONFLICTFLAGS, 80);
+
+ connect(
+ ui->modList->header(), SIGNAL(sectionResized(int, int, int)),
+ conflictFlagDelegate, SLOT(columnResized(int, int, int)));
+
+
+ ui->modList->setItemDelegateForColumn(ModList::COL_FLAGS, flagDelegate);
+ ui->modList->setItemDelegateForColumn(ModList::COL_CONFLICTFLAGS, conflictFlagDelegate);
+ ui->modList->setItemDelegateForColumn(ModList::COL_CONTENT, contentDelegate);
+ ui->modList->header()->installEventFilter(m_OrganizerCore.modList());
+
+
+ if (m_OrganizerCore.settings().geometry().restoreState(ui->modList->header())) {
+ // hack: force the resize-signal to be triggered because restoreState doesn't seem to do that
+ for (int column = 0; column <= ModList::COL_LASTCOLUMN; ++column) {
+ int sectionSize = ui->modList->header()->sectionSize(column);
+ ui->modList->header()->resizeSection(column, sectionSize + 1);
+ ui->modList->header()->resizeSection(column, sectionSize);
+ }
+ } else {
+ // hide these columns by default
+ ui->modList->header()->setSectionHidden(ModList::COL_CONTENT, true);
+ ui->modList->header()->setSectionHidden(ModList::COL_MODID, true);
+ ui->modList->header()->setSectionHidden(ModList::COL_GAME, true);
+ ui->modList->header()->setSectionHidden(ModList::COL_INSTALLTIME, true);
+ ui->modList->header()->setSectionHidden(ModList::COL_NOTES, true);
+
+ // resize mod list to fit content
+ for (int i = 0; i < ui->modList->header()->count(); ++i) {
+ ui->modList->header()->setSectionResizeMode(i, QHeaderView::ResizeToContents);
+ }
+
+ ui->modList->header()->setSectionResizeMode(ModList::COL_NAME, QHeaderView::Stretch);
+ }
+
+ // prevent the name-column from being hidden
+ ui->modList->header()->setSectionHidden(ModList::COL_NAME, false);
+
+ ui->modList->installEventFilter(m_OrganizerCore.modList());
}
void MainWindow::resetActionIcons()
@@ -571,7 +621,7 @@ MainWindow::~MainWindow()
cleanup();
m_PluginContainer.setUserInterface(nullptr, nullptr);
- m_OrganizerCore.setUserInterface(nullptr, nullptr);
+ m_OrganizerCore.setUserInterface(nullptr);
m_IntegratedBrowser.close();
delete ui;
} catch (std::exception &e) {
@@ -599,7 +649,7 @@ void MainWindow::updateWindowTitle(const APIUserAccount& user)
void MainWindow::onRequestsChanged(const APIStats& stats, const APIUserAccount& user)
{
- m_statusBar->setAPI(stats, user);
+ ui->statusBar->setAPI(stats, user);
}
@@ -611,16 +661,8 @@ void MainWindow::disconnectPlugins()
}
-void MainWindow::resizeLists(bool modListCustom, bool pluginListCustom)
+void MainWindow::resizeLists(bool pluginListCustom)
{
- if (!modListCustom) {
- // resize mod list to fit content
- for (int i = 0; i < ui->modList->header()->count(); ++i) {
- ui->modList->header()->setSectionResizeMode(i, QHeaderView::ResizeToContents);
- }
- ui->modList->header()->setSectionResizeMode(ModList::COL_NAME, QHeaderView::Stretch);
- }
-
// ensure the columns aren't so small you can't see them any more
for (int i = 0; i < ui->modList->header()->count(); ++i) {
if (ui->modList->header()->sectionSize(i) < 10) {
@@ -644,15 +686,12 @@ void MainWindow::allowListResize()
for (int i = 0; i < ui->modList->header()->count(); ++i) {
ui->modList->header()->setSectionResizeMode(i, QHeaderView::Interactive);
}
- //ui->modList->header()->setSectionResizeMode(ui->modList->header()->count() - 1, QHeaderView::Stretch);
ui->modList->header()->setStretchLastSection(true);
-
// allow resize on plugin list
for (int i = 0; i < ui->espList->header()->count(); ++i) {
ui->espList->header()->setSectionResizeMode(i, QHeaderView::Interactive);
}
- //ui->espList->header()->setSectionResizeMode(ui->espList->header()->count() - 1, QHeaderView::Stretch);
ui->espList->header()->setStretchLastSection(true);
}
@@ -710,7 +749,11 @@ void MainWindow::setupToolbar()
ui->toolBar->insertWidget(m_linksSeparator, spacer);
} else {
- qWarning("no separator found on the toolbar, icons won't be right-aligned");
+ log::warn("no separator found on the toolbar, icons won't be right-aligned");
+ }
+
+ if (!InstanceManager::instance().allowedToChangeInstance()) {
+ ui->actionChange_Game->setVisible(false);
}
}
@@ -737,7 +780,7 @@ void MainWindow::updatePinnedExecutables()
bool hasLinks = false;
for (const auto& exe : *m_OrganizerCore.executablesList()) {
- if (exe.isShownOnToolbar()) {
+ if (!exe.hide() && exe.isShownOnToolbar()) {
hasLinks = true;
QAction *exeAction = new QAction(
@@ -747,7 +790,7 @@ void MainWindow::updatePinnedExecutables()
exeAction->setStatusTip(exe.binaryInfo().filePath());
if (!connect(exeAction, SIGNAL(triggered()), this, SLOT(startExeAction()))) {
- qDebug("failed to connect trigger?");
+ log::debug("failed to connect trigger?");
}
if (m_linksSeparator) {
@@ -765,17 +808,8 @@ void MainWindow::updatePinnedExecutables()
ui->menuRun->menuAction()->setVisible(hasLinks);
}
-void MainWindow::toolbarMenu_aboutToShow()
+void MainWindow::updateToolbarMenu()
{
- // well, this is a bit of a hack to allow the same toolbar menu to be shown
- // in both the main menu and the context menu
- //
- // the toolbar menu is returned by createPopupMenu(), but Qt takes ownership
- // of it and deletes it by setting the WA_DeleteOnClose attribute on it
- //
- // to avoid deleting the menu, the attribute is removed here
- ui->menuToolbars->setAttribute(Qt::WA_DeleteOnClose, false);
-
ui->actionMainMenuToggle->setChecked(ui->menuBar->isVisible());
ui->actionToolBarMainToggle->setChecked(ui->toolBar->isVisible());
ui->actionStatusBarToggle->setChecked(ui->statusBar->isVisible());
@@ -789,14 +823,35 @@ void MainWindow::toolbarMenu_aboutToShow()
ui->actionToolBarIconsAndText->setChecked(ui->toolBar->toolButtonStyle() == Qt::ToolButtonTextUnderIcon);
}
+void MainWindow::updateViewMenu()
+{
+ ui->actionViewLog->setChecked(ui->logDock->isVisible());
+}
+
QMenu* MainWindow::createPopupMenu()
{
- return ui->menuToolbars;
+ auto* m = new QMenu;
+
+ // add all the actions from the toolbars menu
+ for (auto* a : ui->menuToolbars->actions()) {
+ m->addAction(a);
+ }
+
+ m->addSeparator();
+
+ // other actions
+ m->addAction(ui->actionViewLog);
+
+ // make sure the actions are updated
+ updateToolbarMenu();
+ updateViewMenu();
+
+ return m;
}
void MainWindow::on_actionMainMenuToggle_triggered()
{
- showMenuBar(!ui->menuBar->isVisible());
+ ui->menuBar->setVisible(!ui->menuBar->isVisible());
}
void MainWindow::on_actionToolBarMainToggle_triggered()
@@ -806,7 +861,7 @@ void MainWindow::on_actionToolBarMainToggle_triggered()
void MainWindow::on_actionStatusBarToggle_triggered()
{
- showStatusBar(!ui->statusBar->isVisible());
+ ui->statusBar->setVisible(!ui->statusBar->isVisible());
}
void MainWindow::on_actionToolBarSmallIcons_triggered()
@@ -839,6 +894,11 @@ void MainWindow::on_actionToolBarIconsAndText_triggered()
setToolbarButtonStyle(Qt::ToolButtonTextUnderIcon);
}
+void MainWindow::on_actionViewLog_triggered()
+{
+ ui->logDock->setVisible(!ui->logDock->isVisible());
+}
+
void MainWindow::setToolbarSize(const QSize& s)
{
for (auto* tb : findChildren<QToolBar*>()) {
@@ -853,36 +913,6 @@ void MainWindow::setToolbarButtonStyle(Qt::ToolButtonStyle s)
}
}
-void MainWindow::showMenuBar(bool b)
-{
- ui->menuBar->setVisible(b);
- m_menuBarVisible = b;
-}
-
-void MainWindow::showStatusBar(bool b)
-{
- ui->statusBar->setVisible(b);
- m_statusBarVisible = b;
-
- // the central widget typically has no bottom padding because the status bar
- // is more than enough, but when it's hidden, the bottom widget (currently
- // the log) touches the bottom border of the window, which looks ugly
- //
- // when hiding the statusbar, the central widget is given the same border
- // margin as it has on the top (which is typically 6, as it's the default from
- // the qt designer)
-
- auto m = ui->centralWidget->layout()->contentsMargins();
-
- if (b) {
- m.setBottom(0);
- } else {
- m.setBottom(m.top());
- }
-
- ui->centralWidget->layout()->setContentsMargins(m);
-}
-
void MainWindow::on_centralWidget_customContextMenuRequested(const QPoint &pos)
{
// this allows for getting the context menu even if both the menubar and all
@@ -900,8 +930,7 @@ void MainWindow::on_centralWidget_customContextMenuRequested(const QPoint &pos)
return;
}
- auto* m = createPopupMenu();
- m->exec(ui->centralWidget->mapToGlobal(pos));
+ createPopupMenu()->exec(ui->centralWidget->mapToGlobal(pos));
}
void MainWindow::scheduleUpdateButton()
@@ -964,8 +993,8 @@ void MainWindow::updateProblemsButton()
}
// updating the status bar, may be null very early when MO is starting
- if (m_statusBar) {
- m_statusBar->setNotifications(numProblems > 0);
+ if (ui->statusBar) {
+ ui->statusBar->setNotifications(numProblems > 0);
}
}
@@ -1082,14 +1111,14 @@ void MainWindow::createHelpMenu()
QFile file(dirIter.filePath());
if (!file.open(QIODevice::ReadOnly)) {
- qCritical() << "Failed to open " << fileName;
+ log::error("Failed to open {}", fileName);
continue;
}
QString firstLine = QString::fromUtf8(file.readLine());
if (firstLine.startsWith("//TL")) {
QStringList params = firstLine.mid(4).trimmed().split('#');
if (params.size() != 2) {
- qCritical() << "invalid header line for tutorial " << fileName << " expected 2 parameters";
+ log::error("invalid header line for tutorial {}, expected 2 parameters", fileName);
continue;
}
QAction *tutAction = new QAction(params.at(0), tutorialMenu);
@@ -1193,13 +1222,13 @@ void MainWindow::hookUpWindowTutorials()
QString fileName = dirIter.fileName();
QFile file(dirIter.filePath());
if (!file.open(QIODevice::ReadOnly)) {
- qCritical() << "Failed to open " << fileName;
+ log::error("Failed to open {}", fileName);
continue;
}
QString firstLine = QString::fromUtf8(file.readLine());
if (firstLine.startsWith("//WIN")) {
QString windowName = firstLine.mid(6).trimmed();
- if (!m_OrganizerCore.settings().directInterface().value("CompletedWindowTutorials/" + windowName, false).toBool()) {
+ if (!m_OrganizerCore.settings().interface().isTutorialCompleted(windowName)) {
TutorialManager::instance().activateTutorial(windowName, fileName);
}
}
@@ -1208,11 +1237,12 @@ void MainWindow::hookUpWindowTutorials()
void MainWindow::showEvent(QShowEvent *event)
{
- refreshFilters();
-
QMainWindow::showEvent(event);
if (!m_WasVisible) {
+ readSettings();
+ refreshFilters();
+
// this needs to be connected here instead of in the constructor because the
// actual changing of the stylesheet is done by MOApplication, which
// connects its signal in runApplication() (in main.cpp), and that happens
@@ -1228,7 +1258,7 @@ void MainWindow::showEvent(QShowEvent *event)
hookUpWindowTutorials();
- if (m_OrganizerCore.settings().directInterface().value("first_start", true).toBool()) {
+ if (m_OrganizerCore.settings().firstStart()) {
QString firstStepsTutorial = ToQString(AppConfig::firstStepsTutorial());
if (TutorialManager::instance().hasTutorial(firstStepsTutorial)) {
if (QMessageBox::question(this, tr("Show tutorial?"),
@@ -1239,7 +1269,7 @@ void MainWindow::showEvent(QShowEvent *event)
TutorialManager::instance().activateTutorial("MainWindow", firstStepsTutorial);
}
} else {
- qCritical() << firstStepsTutorial << " missing";
+ log::error("{} missing", firstStepsTutorial);
QPoint pos = ui->toolBar->mapToGlobal(QPoint());
pos.rx() += ui->toolBar->width() / 2;
pos.ry() += ui->toolBar->height();
@@ -1247,53 +1277,103 @@ void MainWindow::showEvent(QShowEvent *event)
QObject::tr("Please use \"Help\" from the toolbar to get usage instructions to all elements"));
}
- m_OrganizerCore.settings().directInterface().setValue("first_start", false);
+ m_OrganizerCore.settings().setFirstStart(false);
}
- // this has no visible impact when called before the ui is visible
- int grouping = m_OrganizerCore.settings().directInterface().value("group_state").toInt();
- ui->groupCombo->setCurrentIndex(grouping);
+ m_OrganizerCore.settings().widgets().restoreIndex(ui->groupCombo);
- allowListResize();
-
- m_OrganizerCore.settings().registerAsNXMHandler(false);
+ m_OrganizerCore.settings().nexus().registerAsNXMHandler(false);
m_WasVisible = true;
- updateProblemsButton();
+ updateProblemsButton();
+ }
+}
+
+void MainWindow::paintEvent(QPaintEvent* event)
+{
+ if (m_FirstPaint) {
+ allowListResize();
+ m_FirstPaint = false;
}
+
+ QMainWindow::paintEvent(event);
}
+void MainWindow::onBeforeClose()
+{
+ storeSettings();
+}
void MainWindow::closeEvent(QCloseEvent* event)
{
- if (!confirmExit()) {
+ if (isVisible()) {
+ // this is messy
+ //
+ // the main problem this is solving is when closing MO, then getting the
+ // lock overlay because processes are still running, then pressing the X
+ // again
+ //
+ // in this case, closeEvent() is _not_ called for the second event and the
+ // window is immediately hidden
+ //
+ // this always saves the settings here; in the event where a lock overlay
+ // is then shown, it might save settings multiple times, but it's harmless
+ onBeforeClose();
+ }
+
+ // this happens for two reasons:
+ // 1) the user requested to close the window, such as clicking the X
+ // 2) close() is called in runApplication() after application.exec()
+ // returns, which happens when qApp->exit() is called
+ //
+ // the window must never actually close for 1), because settings haven't been
+ // saved yet: the state of many widgets is saved to the ini, which relies on
+ // the window still being onscreen (or else everything is considered hidden)
+ //
+ // for 2), the settings have been saved and the window can just close
+
+ if (ModOrganizerCanCloseNow()) {
+ // the user has confirmed if necessary and all settings have been saved,
+ // just close it
+ QMainWindow::closeEvent(event);
+ return;
+ }
+
+ if (UILocker::instance().locked()) {
+ // don't bother asking the user to confirm if the ui is already locked
event->ignore();
+ ExitModOrganizer(Exit::Force);
+ return;
}
+
+ if (ModOrganizerExiting()) {
+ // ignore repeated attempts
+ event->ignore();
+ return;
+ }
+
+ // never close the window because settings might need to be changed
+ event->ignore();
+
+ // start the process of exiting, which may require confirmation by calling
+ // canExit(), among other things
+ ExitModOrganizer();
}
-bool MainWindow::confirmExit()
+bool MainWindow::canExit()
{
- m_closing = true;
-
if (m_OrganizerCore.downloadManager()->downloadsInProgressNoPause()) {
if (QMessageBox::question(this, tr("Downloads in progress"),
tr("There are still downloads in progress, do you really want to quit?"),
QMessageBox::Yes | QMessageBox::Cancel) == QMessageBox::Cancel) {
- m_closing = false;
return false;
} else {
m_OrganizerCore.downloadManager()->pauseAll();
}
}
- std::vector<QString> hiddenList;
- hiddenList.push_back(QFileInfo(QCoreApplication::applicationFilePath()).fileName());
- HANDLE injected_process_still_running = m_OrganizerCore.findAndOpenAUSVFSProcess(hiddenList, GetCurrentProcessId());
- if (injected_process_still_running != INVALID_HANDLE_VALUE)
- {
- m_OrganizerCore.waitForApplication(injected_process_still_running);
- if (!m_closing) { // if operation cancelled
- return false;
- }
+ const auto r = m_OrganizerCore.waitForAllUSVFSProcesses();
+ if (r == ProcessRunner::Cancelled) {
+ return false;
}
setCursor(Qt::WaitCursor);
@@ -1302,23 +1382,12 @@ bool MainWindow::confirmExit()
void MainWindow::cleanup()
{
- if (ui->logList->model() != nullptr) {
- disconnect(ui->logList->model(), nullptr, nullptr, nullptr);
- ui->logList->setModel(nullptr);
- }
-
QWebEngineProfile::defaultProfile()->clearAllVisitedLinks();
m_IntegratedBrowser.close();
m_SaveMetaTimer.stop();
m_MetaSave.waitForFinished();
}
-
-void MainWindow::setBrowserGeometry(const QByteArray &geometry)
-{
- m_IntegratedBrowser.restoreGeometry(geometry);
-}
-
void MainWindow::displaySaveGameInfo(QListWidgetItem *newItem)
{
// don't display the widget if the main window doesn't have focus
@@ -1343,7 +1412,12 @@ void MainWindow::displaySaveGameInfo(QListWidgetItem *newItem)
}
m_CurrentSaveView->setSave(save);
- QRect screenRect = QApplication::desktop()->availableGeometry(m_CurrentSaveView);
+ QWindow *window = m_CurrentSaveView->window()->windowHandle();
+ QRect screenRect;
+ if (window == nullptr)
+ screenRect = QGuiApplication::primaryScreen()->geometry();
+ else
+ screenRect = window->screen()->geometry();
QPoint pos = QCursor::pos();
if (pos.x() + m_CurrentSaveView->width() > screenRect.right()) {
@@ -1506,7 +1580,7 @@ void MainWindow::startExeAction()
QAction *action = qobject_cast<QAction*>(sender());
if (action == nullptr) {
- qCritical("not an action?");
+ log::error("not an action?");
return;
}
@@ -1516,45 +1590,17 @@ void MainWindow::startExeAction()
auto itor = list.find(title);
if (itor == list.end()) {
- qWarning().nospace()
- << "startExeAction(): executable '" << title << "' not found";
-
+ log::warn("startExeAction(): executable '{}' not found", title);
return;
}
action->setEnabled(false);
- const Executable& exe = *itor;
- auto& profile = *m_OrganizerCore.currentProfile();
-
- QString customOverwrite = profile.setting("custom_overwrites", exe.title()).toString();
- auto forcedLibraries = profile.determineForcedLibraries(exe.title());
-
- if (!profile.forcedLibrariesEnabled(exe.title())) {
- forcedLibraries.clear();
- }
-
- m_OrganizerCore.spawnBinary(
- exe.binaryInfo(), exe.arguments(),
- exe.workingDirectory().length() != 0
- ? exe.workingDirectory()
- : exe.binaryInfo().absolutePath(),
- exe.steamAppID(),
- customOverwrite,
- forcedLibraries);
- action->setEnabled(true);
+ Guard g([&]{ action->setEnabled(true); });
-}
-
-
-void MainWindow::setExecutableIndex(int index)
-{
- QComboBox *executableBox = findChild<QComboBox*>("executablesListBox");
-
- if ((index != 0) && (executableBox->count() > index)) {
- executableBox->setCurrentIndex(index);
- } else {
- executableBox->setCurrentIndex(1);
- }
+ m_OrganizerCore.processRunner()
+ .setFromExecutable(*itor)
+ .setWaitForCompletion(ProcessRunner::Refresh)
+ .run();
}
void MainWindow::activateSelectedProfile()
@@ -1583,7 +1629,7 @@ void MainWindow::on_profileBox_currentIndexChanged(int index)
// ensure the new index is valid
if (index < 0 || index >= ui->profileBox->count()) {
- qDebug("invalid profile index, using last profile");
+ log::debug("invalid profile index, using last profile");
ui->profileBox->setCurrentIndex(ui->profileBox->count() - 1);
}
@@ -1675,9 +1721,13 @@ void MainWindow::updateTo(QTreeWidgetItem *subTree, const std::wstring &director
QString fileName = ToQString(current->getName());
QStringList columns(fileName);
FilesOrigin origin = m_OrganizerCore.directoryStructure()->getOriginByID(originID);
- QString source("data");
- unsigned int modIndex = ModInfo::getIndex(ToQString(origin.getName()));
- if (modIndex != UINT_MAX) {
+
+ QString source;
+ const unsigned int modIndex = ModInfo::getIndex(ToQString(origin.getName()));
+
+ if (modIndex == UINT_MAX) {
+ source = UnmanagedModName();
+ } else {
ModInfo::Ptr modInfo = ModInfo::getByIndex(modIndex);
source = modInfo->name();
}
@@ -1744,24 +1794,24 @@ void MainWindow::expandDataTreeItem(QTreeWidgetItem *item)
if ((item->childCount() == 1) && (item->child(0)->data(0, Qt::UserRole).toString() == "__loaded_on_demand__")) {
// read the data we need from the sub-item, then dispose of it
QTreeWidgetItem *onDemandDataItem = item->child(0);
- std::wstring path = ToWString(onDemandDataItem->data(0, Qt::UserRole + 1).toString());
+ const QString path = onDemandDataItem->data(0, Qt::UserRole + 1).toString();
+ std::wstring wspath = path.toStdWString();
bool conflictsOnly = onDemandDataItem->data(0, Qt::UserRole + 2).toBool();
- std::wstring virtualPath = (path + L"\\").substr(6) + ToWString(item->text(0));
+ std::wstring virtualPath = (wspath + L"\\").substr(6) + ToWString(item->text(0));
DirectoryEntry *dir = m_OrganizerCore.directoryStructure()->findSubDirectoryRecursive(virtualPath);
if (dir != nullptr) {
QIcon folderIcon = (new QFileIconProvider())->icon(QFileIconProvider::Folder);
QIcon fileIcon = (new QFileIconProvider())->icon(QFileIconProvider::File);
- updateTo(item, path, *dir, conflictsOnly, &fileIcon, &folderIcon);
+ updateTo(item, wspath, *dir, conflictsOnly, &fileIcon, &folderIcon);
} else {
- qWarning("failed to update view of %ls", path.c_str());
+ log::warn("failed to update view of {}", path);
}
m_RemoveWidget.push_back(item);
QTimer::singleShot(5, this, SLOT(delayedRemove()));
}
}
-
bool MainWindow::refreshProfiles(bool selectProfile)
{
QComboBox* profileBox = findChild<QComboBox*>("profileBox");
@@ -1772,7 +1822,7 @@ bool MainWindow::refreshProfiles(bool selectProfile)
profileBox->clear();
profileBox->addItem(QObject::tr("<Manage...>"));
- QDir profilesDir(Settings::instance().getProfileDirectory());
+ QDir profilesDir(Settings::instance().paths().profiles());
profilesDir.setFilter(QDir::AllDirs | QDir::NoDotAndDotDot);
QDirIterator profileIter(profilesDir);
@@ -1803,23 +1853,44 @@ bool MainWindow::refreshProfiles(bool selectProfile)
void MainWindow::refreshExecutablesList()
{
- QComboBox* executablesList = findChild<QComboBox*>("executablesListBox");
- executablesList->setEnabled(false);
- executablesList->clear();
- executablesList->addItem(tr("<Edit...>"));
+ QAbstractItemModel *model = ui->executablesListBox->model();
+
+ auto add = [&](const QString& title, const QFileInfo& binary) {
+ QIcon icon;
+ if (!binary.fileName().isEmpty()) {
+ icon = iconForExecutable(binary.filePath());
+ }
+
+ ui->executablesListBox->addItem(icon, title);
+
+ const auto i = ui->executablesListBox->count() - 1;
+
+ model->setData(
+ model->index(i, 0),
+ QSize(0, ui->executablesListBox->iconSize().height() + 4),
+ Qt::SizeHintRole);
+ };
- QAbstractItemModel *model = executablesList->model();
- int i = 0;
+ ui->executablesListBox->setEnabled(false);
+ ui->executablesListBox->clear();
+
+ add(tr("<Edit...>"), {});
+
for (const auto& exe : *m_OrganizerCore.executablesList()) {
- QIcon icon = iconForExecutable(exe.binaryInfo().filePath());
- executablesList->addItem(icon, exe.title());
- model->setData(model->index(i, 0), QSize(0, executablesList->iconSize().height() + 4), Qt::SizeHintRole);
- ++i;
+ if (!exe.hide()) {
+ add(exe.title(), exe.binaryInfo());
+ }
+ }
+
+ if (ui->executablesListBox->count() == 1) {
+ // all executables are hidden, add an empty one to at least be able to
+ // switch to edit
+ add(tr("(no executables)"), QFileInfo(":badfile"));
}
- setExecutableIndex(1);
- executablesList->setEnabled(true);
+ ui->executablesListBox->setCurrentIndex(1);
+ ui->executablesListBox->setEnabled(true);
}
@@ -1931,7 +2002,7 @@ void MainWindow::refreshSaveList()
QDir savesDir = currentSavesDir();
savesDir.setNameFilters(filters);
- qDebug("reading save games from %s", qUtf8Printable(savesDir.absolutePath()));
+ log::debug("reading save games from {}", savesDir.absolutePath());
QFileInfoList files = savesDir.entryInfoList(QDir::Files, QDir::Time);
for (const QFileInfo &file : files) {
@@ -2011,7 +2082,7 @@ void MainWindow::updateBSAList(const QStringList &defaultArchives, const QString
newItem->setFlags(newItem->flags() & ~(Qt::ItemIsDropEnabled | Qt::ItemIsUserCheckable));
newItem->setCheckState(0, (index != -1) ? Qt::Checked : Qt::Unchecked);
newItem->setData(0, Qt::UserRole, false);
- if (m_OrganizerCore.settings().forceEnableCoreFiles()
+ if (m_OrganizerCore.settings().game().forceEnableCoreFiles()
&& defaultArchives.contains(fileInfo.fileName())) {
newItem->setCheckState(0, Qt::Checked);
newItem->setDisabled(true);
@@ -2038,12 +2109,17 @@ void MainWindow::updateBSAList(const QStringList &defaultArchives, const QString
int originID = iter->second->data(1, Qt::UserRole).toInt();
FilesOrigin origin = m_OrganizerCore.directoryStructure()->getOriginByID(originID);
- QString modName("data");
- unsigned int modIndex = ModInfo::getIndex(ToQString(origin.getName()));
- if (modIndex != UINT_MAX) {
+
+ QString modName;
+ const unsigned int modIndex = ModInfo::getIndex(ToQString(origin.getName()));
+
+ if (modIndex == UINT_MAX) {
+ modName = UnmanagedModName();
+ } else {
ModInfo::Ptr modInfo = ModInfo::getByIndex(modIndex);
modName = modInfo->name();
}
+
QList<QTreeWidgetItem*> items = ui->bsaList->findItems(modName, Qt::MatchFixedString);
QTreeWidgetItem * subItem = nullptr;
if (items.length() > 0) {
@@ -2132,15 +2208,6 @@ void MainWindow::fixCategories()
void MainWindow::setupNetworkProxy(bool activate)
{
QNetworkProxyFactory::setUseSystemConfiguration(activate);
-/* QNetworkProxyQuery query(QUrl("http://www.google.com"), QNetworkProxyQuery::UrlRequest);
- query.setProtocolTag("http");
- QList<QNetworkProxy> proxies = QNetworkProxyFactory::systemProxyForQuery(query);
- if ((proxies.size() > 0) && (proxies.at(0).type() != QNetworkProxy::NoProxy)) {
- qDebug("Using proxy: %s", qUtf8Printable(proxies.at(0).hostName()));
- QNetworkProxy::setApplicationProxy(proxies[0]);
- } else {
- qDebug("Not using proxy");
- }*/
}
@@ -2160,84 +2227,59 @@ void MainWindow::activateProxy(bool activate)
void MainWindow::readSettings()
{
- QSettings settings(qApp->property("dataPath").toString() + "/" + QString::fromStdWString(AppConfig::iniFileName()), QSettings::IniFormat);
+ const auto& s = m_OrganizerCore.settings();
- if (settings.contains("window_geometry")) {
- restoreGeometry(settings.value("window_geometry").toByteArray());
+ if (!s.geometry().restoreGeometry(this)) {
+ resize(1300, 800);
}
- if (settings.contains("window_state")) {
- restoreState(settings.value("window_state").toByteArray());
- }
-
- if (settings.contains("toolbar_size")) {
- setToolbarSize(settings.value("toolbar_size").toSize());
- }
+ s.geometry().restoreState(this);
+ s.geometry().restoreDocks(this);
+ s.geometry().restoreToolbars(this);
+ s.geometry().restoreState(ui->splitter);
+ s.geometry().restoreState(ui->categoriesSplitter);
+ s.geometry().restoreVisibility(ui->menuBar);
+ s.geometry().restoreVisibility(ui->statusBar);
- if (settings.contains("toolbar_button_style")) {
- setToolbarButtonStyle(static_cast<Qt::ToolButtonStyle>(
- settings.value("toolbar_button_style").toInt()));
- }
-
- if (settings.contains("menubar_visible")) {
- showMenuBar(settings.value("menubar_visible").toBool());
- }
+ {
+ // special case in case someone puts 0 in the INI
+ auto v = s.widgets().index(ui->executablesListBox);
+ if (!v || v == 0) {
+ v = 1;
+ }
- if (settings.contains("statusbar_visible")) {
- showStatusBar(settings.value("statusbar_visible").toBool());
+ ui->executablesListBox->setCurrentIndex(*v);
}
- if (settings.contains("window_split")) {
- ui->splitter->restoreState(settings.value("window_split").toByteArray());
- }
+ s.widgets().restoreIndex(ui->groupCombo);
+ m_Filters->restoreState(s);
- if (settings.contains("log_split")) {
- ui->topLevelSplitter->restoreState(settings.value("log_split").toByteArray());
+ {
+ s.geometry().restoreVisibility(ui->categoriesGroup, false);
+ const auto v = ui->categoriesGroup->isVisible();
+ setCategoryListVisible(v);
+ ui->displayCategoriesBtn->setChecked(v);
}
- bool filtersVisible = settings.value("filters_visible", false).toBool();
- setCategoryListVisible(filtersVisible);
- ui->displayCategoriesBtn->setChecked(filtersVisible);
-
- int selectedExecutable = settings.value("selected_executable").toInt();
- setExecutableIndex(selectedExecutable);
-
- if (settings.value("Settings/use_proxy", false).toBool()) {
+ if (s.network().useProxy()) {
activateProxy(true);
}
}
void MainWindow::processUpdates() {
- QSettings settings(qApp->property("dataPath").toString() + "/" + QString::fromStdWString(AppConfig::iniFileName()), QSettings::IniFormat);
- QVersionNumber lastVersion = QVersionNumber::fromString(settings.value("version", "2.1.2").toString()).normalized();
- QVersionNumber currentVersion = QVersionNumber::fromString(m_OrganizerCore.getVersion().displayString()).normalized();
- if (!m_OrganizerCore.settings().directInterface().value("first_start", true).toBool()) {
- if (lastVersion < QVersionNumber(2, 1, 3)) {
- bool lastHidden = true;
- for (int i = ModList::COL_GAME; i < ui->modList->model()->columnCount(); ++i) {
- bool hidden = ui->modList->header()->isSectionHidden(i);
- ui->modList->header()->setSectionHidden(i, lastHidden);
- lastHidden = hidden;
- }
- }
+ auto& settings = m_OrganizerCore.settings();
+ const auto earliest = QVersionNumber::fromString("2.1.2").normalized();
+
+ const auto lastVersion = settings.version().value_or(earliest);
+ const auto currentVersion = m_OrganizerCore.getVersion().asQVersionNumber();
+
+ settings.processUpdates(currentVersion, lastVersion);
+
+ if (!settings.firstStart()) {
if (lastVersion < QVersionNumber(2, 1, 6)) {
ui->modList->header()->setSectionHidden(ModList::COL_NOTES, true);
}
- if (lastVersion < QVersionNumber(2, 2, 0)) {
- QSettings &instance = Settings::instance().directInterface();
- instance.beginGroup("Settings");
- instance.remove("steam_password");
- instance.remove("nexus_username");
- instance.remove("nexus_password");
- instance.remove("nexus_login");
- instance.remove("nexus_api_key");
- instance.remove("ask_for_nexuspw");
- instance.remove("nmm_version");
- instance.endGroup();
- instance.beginGroup("Servers");
- instance.remove("");
- instance.endGroup();
- }
+
if (lastVersion < QVersionNumber(2, 2, 1)) {
// hide new columns by default
for (int i=DownloadList::COL_MODNAME; i<DownloadList::COL_COUNT; ++i) {
@@ -2246,87 +2288,48 @@ void MainWindow::processUpdates() {
}
}
- if (currentVersion > lastVersion) {
- //NOP
- } else if (currentVersion < lastVersion)
- qWarning() << tr("Notice: Your current MO version (%1) is lower than the previously used one (%2). "
- "The GUI may not downgrade gracefully, so you may experience oddities. "
- "However, there should be no serious issues.").arg(currentVersion.toString()).arg(lastVersion.toString()).toStdWString();
- //save version in all case
- settings.setValue("version", currentVersion.toString());
-}
-
-void MainWindow::storeSettings(QSettings &settings) {
- settings.setValue("group_state", ui->groupCombo->currentIndex());
- settings.setValue("selected_executable",
- ui->executablesListBox->currentIndex());
+ if (currentVersion < lastVersion) {
+ const auto text = tr(
+ "Notice: Your current MO version (%1) is lower than the previously used one (%2). "
+ "The GUI may not downgrade gracefully, so you may experience oddities. "
+ "However, there should be no serious issues.")
+ .arg(currentVersion.toString())
+ .arg(lastVersion.toString());
- if (settings.value("reset_geometry", false).toBool()) {
- settings.remove("window_geometry");
- settings.remove("window_state");
- settings.remove("toolbar_size");
- settings.remove("toolbar_button_style");
- settings.remove("menubar_visible");
- settings.remove("window_split");
- settings.remove("window_monitor");
- settings.remove("log_split");
- settings.remove("filters_visible");
- settings.remove("browser_geometry");
- settings.remove("geometry");
- settings.remove("reset_geometry");
- } else {
- settings.setValue("window_geometry", saveGeometry());
- settings.setValue("window_state", saveState());
- settings.setValue("toolbar_size", ui->toolBar->iconSize());
- settings.setValue("toolbar_button_style", static_cast<int>(ui->toolBar->toolButtonStyle()));
- settings.setValue("menubar_visible", m_menuBarVisible);
- settings.setValue("statusbar_visible", m_statusBarVisible);
- settings.setValue("window_split", ui->splitter->saveState());
- settings.setValue("window_monitor", QApplication::desktop()->screenNumber(this));
- settings.setValue("log_split", ui->topLevelSplitter->saveState());
- settings.setValue("browser_geometry", m_IntegratedBrowser.saveGeometry());
- settings.setValue("filters_visible", ui->displayCategoriesBtn->isChecked());
- for (const std::pair<QString, QHeaderView*> kv : m_PersistedGeometry) {
- QString key = QString("geometry/") + kv.first;
- settings.setValue(key, kv.second->saveState());
- }
+ log::warn("{}", text);
}
}
-ILockedWaitingForProcess* MainWindow::lock()
+void MainWindow::storeSettings()
{
- if (m_LockDialog != nullptr) {
- ++m_LockCount;
- return m_LockDialog;
- }
- if (m_closing)
- m_LockDialog = new WaitingOnCloseDialog(this);
- else
- m_LockDialog = new LockedDialog(this, true);
- m_LockDialog->setModal(true);
- m_LockDialog->show();
- setEnabled(false);
- m_LockDialog->setEnabled(true); //What's the point otherwise?
- ++m_LockCount;
- return m_LockDialog;
+ auto& s = m_OrganizerCore.settings();
+
+ s.geometry().saveState(this);
+ s.geometry().saveGeometry(this);
+ s.geometry().saveDocks(this);
+
+ s.geometry().saveVisibility(ui->menuBar);
+ s.geometry().saveVisibility(ui->statusBar);
+ s.geometry().saveToolbars(this);
+ s.geometry().saveState(ui->splitter);
+ s.geometry().saveState(ui->categoriesSplitter);
+ s.geometry().saveMainWindowMonitor(this);
+ s.geometry().saveVisibility(ui->categoriesGroup);
+
+ s.geometry().saveState(ui->espList->header());
+ s.geometry().saveState(ui->dataTree->header());
+ s.geometry().saveState(ui->downloadView->header());
+ s.geometry().saveState(ui->modList->header());
+
+ s.widgets().saveIndex(ui->groupCombo);
+ s.widgets().saveIndex(ui->executablesListBox);
+
+ m_Filters->saveState(s);
}
-void MainWindow::unlock()
+QWidget* MainWindow::qtWidget()
{
- //If you come through here with a null lock pointer, it's a bug!
- if (m_LockDialog == nullptr) {
- qDebug("Unlocking main window when already unlocked");
- return;
- }
- --m_LockCount;
- if (m_LockCount == 0) {
- if (m_closing && m_LockDialog->canceled())
- m_closing = false;
- m_LockDialog->hide();
- m_LockDialog->deleteLater();
- m_LockDialog = nullptr;
- setEnabled(true);
- }
+ return this;
}
void MainWindow::on_btnRefreshData_clicked()
@@ -2376,72 +2379,59 @@ void MainWindow::installMod(QString fileName)
}
}
-void MainWindow::on_startButton_clicked() {
- ui->startButton->setEnabled(false);
- try {
- const Executable &selectedExecutable(getSelectedExecutable());
- QString customOverwrite = m_OrganizerCore.currentProfile()->setting("custom_overwrites", selectedExecutable.title()).toString();
- auto forcedLibraries = m_OrganizerCore.currentProfile()->determineForcedLibraries(selectedExecutable.title());
- if (!m_OrganizerCore.currentProfile()->forcedLibrariesEnabled(selectedExecutable.title())) {
- forcedLibraries.clear();
- }
- m_OrganizerCore.spawnBinary(
- selectedExecutable.binaryInfo(), selectedExecutable.arguments(),
- selectedExecutable.workingDirectory().length() != 0
- ? selectedExecutable.workingDirectory()
- : selectedExecutable.binaryInfo().absolutePath(),
- selectedExecutable.steamAppID(),
- customOverwrite,
- forcedLibraries);
- } catch (...) {
- ui->startButton->setEnabled(true);
- throw;
+void MainWindow::on_startButton_clicked()
+{
+ const Executable* selectedExecutable = getSelectedExecutable();
+ if (!selectedExecutable) {
+ return;
}
- ui->startButton->setEnabled(true);
+
+ ui->startButton->setEnabled(false);
+ Guard g([&]{ ui->startButton->setEnabled(true); });
+
+ m_OrganizerCore.processRunner()
+ .setFromExecutable(*selectedExecutable)
+ .setWaitForCompletion(ProcessRunner::Refresh)
+ .run();
}
-bool MainWindow::modifyExecutablesDialog()
+bool MainWindow::modifyExecutablesDialog(int selection)
{
bool result = false;
try {
- const auto oldExecutables = *m_OrganizerCore.executablesList();
-
- EditExecutablesDialog dialog(m_OrganizerCore, this);
-
- QSettings &settings = m_OrganizerCore.settings().directInterface();
- QString key = QString("geometry/%1").arg(dialog.objectName());
-
- if (settings.contains(key)) {
- dialog.restoreGeometry(settings.value(key).toByteArray());
- }
+ EditExecutablesDialog dialog(m_OrganizerCore, selection, this);
result = (dialog.exec() == QDialog::Accepted);
- settings.setValue(key, dialog.saveGeometry());
refreshExecutablesList();
updatePinnedExecutables();
} catch (const std::exception &e) {
reportError(e.what());
}
+
return result;
}
void MainWindow::on_executablesListBox_currentIndexChanged(int index)
{
- QComboBox* executablesList = findChild<QComboBox*>("executablesListBox");
+ if (!ui->executablesListBox->isEnabled()) {
+ return;
+ }
+
+ const int previousIndex =
+ (m_OldExecutableIndex > 0 ? m_OldExecutableIndex : 1);
- int previousIndex = m_OldExecutableIndex;
m_OldExecutableIndex = index;
- if (executablesList->isEnabled()) {
- //I think the 2nd test is impossible
- if ((index == 0) || (index > static_cast<int>(m_OrganizerCore.executablesList()->size()))) {
- if (modifyExecutablesDialog()) {
- setExecutableIndex(previousIndex);
- }
+ if (index == 0) {
+ modifyExecutablesDialog(previousIndex - 1);
+ const auto newCount = ui->executablesListBox->count();
+
+ if (previousIndex >= 0 && previousIndex < newCount) {
+ ui->executablesListBox->setCurrentIndex(previousIndex);
} else {
- setExecutableIndex(index);
+ ui->executablesListBox->setCurrentIndex(newCount - 1);
}
}
}
@@ -2490,17 +2480,13 @@ void MainWindow::on_actionAdd_Profile_triggered()
ProfilesDialog profilesDialog(m_OrganizerCore.currentProfile()->name(),
m_OrganizerCore.managedGame(),
this);
- QSettings &settings = m_OrganizerCore.settings().directInterface();
- QString key = QString("geometry/%1").arg(profilesDialog.objectName());
- if (settings.contains(key)) {
- profilesDialog.restoreGeometry(settings.value(key).toByteArray());
- }
+
// workaround: need to disable monitoring of the saves directory, otherwise the active
// profile directory is locked
stopMonitorSaves();
profilesDialog.exec();
- settings.setValue(key, profilesDialog.saveGeometry());
refreshSaveList(); // since the save list may now be outdated we have to refresh it completely
+
if (refreshProfiles() && !profilesDialog.failed()) {
break;
}
@@ -2521,8 +2507,15 @@ void MainWindow::on_actionAdd_Profile_triggered()
void MainWindow::on_actionModify_Executables_triggered()
{
- if (modifyExecutablesDialog()) {
- setExecutableIndex(m_OldExecutableIndex);
+ const auto sel = (m_OldExecutableIndex > 0 ? m_OldExecutableIndex - 1 : 0);
+
+ if (modifyExecutablesDialog(sel)) {
+ const auto newCount = ui->executablesListBox->count();
+ if (m_OldExecutableIndex >= 0 && m_OldExecutableIndex < newCount) {
+ ui->executablesListBox->setCurrentIndex(m_OldExecutableIndex);
+ } else {
+ ui->executablesListBox->setCurrentIndex(newCount - 1);
+ }
}
}
@@ -2556,7 +2549,7 @@ void MainWindow::setESPListSorting(int index)
void MainWindow::refresher_progress(int percent)
{
setEnabled(percent == 100);
- m_statusBar->setProgress(percent);
+ ui->statusBar->setProgress(percent);
}
void MainWindow::directory_refreshed()
@@ -2570,7 +2563,7 @@ void MainWindow::directory_refreshed()
if (ui->tabWidget->currentIndex() == 2) {
refreshDataTreeKeepExpandedNodes();
}
-
+
}
void MainWindow::esplist_changed()
@@ -2643,17 +2636,6 @@ void MainWindow::modInstalled(const QString &modName)
modUpdateCheck(IDs);
}
-void MainWindow::procError(QProcess::ProcessError error)
-{
- reportError(tr("failed to spawn notepad.exe: %1").arg(error));
- this->sender()->deleteLater();
-}
-
-void MainWindow::procFinished(int, QProcess::ExitStatus)
-{
- this->sender()->deleteLater();
-}
-
void MainWindow::showMessage(const QString &message)
{
MessageDialog::showMessage(message, this);
@@ -2716,106 +2698,6 @@ void MainWindow::fileMoved(const QString &filePath, const QString &oldOriginName
}
}
-QTreeWidgetItem *MainWindow::addFilterItem(QTreeWidgetItem *root, const QString &name, int categoryID, ModListSortProxy::FilterType type)
-{
- QTreeWidgetItem *item = new QTreeWidgetItem(QStringList(name));
- item->setData(0, Qt::ToolTipRole, name);
- item->setData(0, Qt::UserRole, categoryID);
- item->setData(0, Qt::UserRole + 1, type);
- if (root != nullptr) {
- root->addChild(item);
- } else {
- ui->categoriesList->addTopLevelItem(item);
- }
- return item;
-}
-
-void MainWindow::addContentFilters()
-{
- for (unsigned i = 0; i < ModInfo::NUM_CONTENT_TYPES; ++i) {
- addFilterItem(nullptr, tr("<Contains %1>").arg(ModInfo::getContentTypeName(i)), i, ModListSortProxy::TYPE_CONTENT);
- }
-}
-
-void MainWindow::addCategoryFilters(QTreeWidgetItem *root, const std::set<int> &categoriesUsed, int targetID)
-{
- for (unsigned int i = 1;
- i < static_cast<unsigned int>(m_CategoryFactory.numCategories()); ++i) {
- if ((m_CategoryFactory.getParentID(i) == targetID)) {
- int categoryID = m_CategoryFactory.getCategoryID(i);
- if (categoriesUsed.find(categoryID) != categoriesUsed.end()) {
- QTreeWidgetItem *item =
- addFilterItem(root, m_CategoryFactory.getCategoryName(i),
- categoryID, ModListSortProxy::TYPE_CATEGORY);
- if (m_CategoryFactory.hasChildren(i)) {
- addCategoryFilters(item, categoriesUsed, categoryID);
- }
- }
- }
- }
-}
-
-void MainWindow::refreshFilters()
-{
- QItemSelection currentSelection = ui->modList->selectionModel()->selection();
-
- QVariant currentIndexName = ui->modList->currentIndex().data();
- ui->modList->setCurrentIndex(QModelIndex());
-
- QStringList selectedItems;
- for (QTreeWidgetItem *item : ui->categoriesList->selectedItems()) {
- selectedItems.append(item->text(0));
- }
-
- ui->categoriesList->clear();
- addFilterItem(nullptr, tr("<Checked>"), CategoryFactory::CATEGORY_SPECIAL_CHECKED, ModListSortProxy::TYPE_SPECIAL);
- addFilterItem(nullptr, tr("<Unchecked>"), CategoryFactory::CATEGORY_SPECIAL_UNCHECKED, ModListSortProxy::TYPE_SPECIAL);
- addFilterItem(nullptr, tr("<Update>"), CategoryFactory::CATEGORY_SPECIAL_UPDATEAVAILABLE, ModListSortProxy::TYPE_SPECIAL);
- addFilterItem(nullptr, tr("<Mod Backup>"), CategoryFactory::CATEGORY_SPECIAL_BACKUP, ModListSortProxy::TYPE_SPECIAL);
- addFilterItem(nullptr, tr("<Managed by MO>"), CategoryFactory::CATEGORY_SPECIAL_MANAGED, ModListSortProxy::TYPE_SPECIAL);
- addFilterItem(nullptr, tr("<Managed outside MO>"), CategoryFactory::CATEGORY_SPECIAL_UNMANAGED, ModListSortProxy::TYPE_SPECIAL);
- addFilterItem(nullptr, tr("<No category>"), CategoryFactory::CATEGORY_SPECIAL_NOCATEGORY, ModListSortProxy::TYPE_SPECIAL);
- addFilterItem(nullptr, tr("<Conflicted>"), CategoryFactory::CATEGORY_SPECIAL_CONFLICT, ModListSortProxy::TYPE_SPECIAL);
- addFilterItem(nullptr, tr("<Not Endorsed>"), CategoryFactory::CATEGORY_SPECIAL_NOTENDORSED, ModListSortProxy::TYPE_SPECIAL);
-
- addContentFilters();
- std::set<int> categoriesUsed;
- for (unsigned int modIdx = 0; modIdx < ModInfo::getNumMods(); ++modIdx) {
- ModInfo::Ptr modInfo = ModInfo::getByIndex(modIdx);
- for (int categoryID : modInfo->getCategories()) {
- int currentID = categoryID;
- std::set<int> cycleTest;
- // also add parents so they show up in the tree
- while (currentID != 0) {
- categoriesUsed.insert(currentID);
- if (!cycleTest.insert(currentID).second) {
- qWarning("cycle in categories: %s", qUtf8Printable(SetJoin(cycleTest, ", ")));
- break;
- }
- currentID = m_CategoryFactory.getParentID(m_CategoryFactory.getCategoryIndex(currentID));
- }
- }
- }
-
- addCategoryFilters(nullptr, categoriesUsed, 0);
-
- for (const QString &item : selectedItems) {
- QList<QTreeWidgetItem*> matches = ui->categoriesList->findItems(item, Qt::MatchFixedString | Qt::MatchRecursive);
- if (matches.size() > 0) {
- matches.at(0)->setSelected(true);
- }
- }
- ui->modList->selectionModel()->select(currentSelection, QItemSelectionModel::Select);
- QModelIndexList matchList;
- if (currentIndexName.isValid()) {
- matchList = ui->modList->model()->match(ui->modList->model()->index(0, 0), Qt::DisplayRole, currentIndexName);
- }
-
- if (matchList.size() > 0) {
- ui->modList->setCurrentIndex(matchList.at(0));
- }
-}
-
void MainWindow::renameMod_clicked()
{
@@ -2833,7 +2715,7 @@ void MainWindow::restoreBackup_clicked()
ModInfo::Ptr modInfo = ModInfo::getByIndex(m_ContextRow);
if (backupRegEx.indexIn(modInfo->name()) != -1) {
QString regName = backupRegEx.cap(1);
- QDir modDir(QDir::fromNativeSeparators(m_OrganizerCore.settings().getModDirectory()));
+ QDir modDir(QDir::fromNativeSeparators(m_OrganizerCore.settings().paths().mods()));
if (!modDir.exists(regName) ||
(QMessageBox::question(this, tr("Overwrite?"),
tr("This will replace the existing mod \"%1\". Continue?").arg(regName),
@@ -2841,7 +2723,7 @@ void MainWindow::restoreBackup_clicked()
if (modDir.exists(regName) && !shellDelete(QStringList(modDir.absoluteFilePath(regName)))) {
reportError(tr("failed to remove mod \"%1\"").arg(regName));
} else {
- QString destinationPath = QDir::fromNativeSeparators(m_OrganizerCore.settings().getModDirectory()) + "/" + regName;
+ QString destinationPath = QDir::fromNativeSeparators(m_OrganizerCore.settings().paths().mods()) + "/" + regName;
if (!modDir.rename(modInfo->absolutePath(), destinationPath)) {
reportError(tr("failed to rename \"%1\" to \"%2\"").arg(modInfo->absolutePath()).arg(destinationPath));
}
@@ -3095,14 +2977,9 @@ void MainWindow::untrack_clicked()
});
}
-void MainWindow::validationFailed(const QString &error)
-{
- qDebug("Nexus API validation failed: %s", qUtf8Printable(error));
-}
-
void MainWindow::windowTutorialFinished(const QString &windowName)
{
- m_OrganizerCore.settings().directInterface().setValue(QString("CompletedWindowTutorials/") + windowName, true);
+ m_OrganizerCore.settings().interface().setTutorialCompleted(windowName);
}
void MainWindow::overwriteClosed(int)
@@ -3110,9 +2987,6 @@ void MainWindow::overwriteClosed(int)
OverwriteInfoDialog *dialog = this->findChild<OverwriteInfoDialog*>("__overwriteDialog");
if (dialog != nullptr) {
m_OrganizerCore.modList()->modInfoChanged(dialog->modInfo());
- QSettings &settings = m_OrganizerCore.settings().directInterface();
- QString key = QString("geometry/%1").arg(dialog->objectName());
- settings.setValue(key, dialog->saveGeometry());
dialog->deleteLater();
}
m_OrganizerCore.refreshDirectoryStructure();
@@ -3123,7 +2997,7 @@ void MainWindow::displayModInformation(
ModInfo::Ptr modInfo, unsigned int modIndex, ModInfoTabIDs tabID)
{
if (!m_OrganizerCore.modList()->modInfoAboutToChange(modInfo)) {
- qDebug("A different mod information dialog is open. If this is incorrect, please restart MO");
+ log::debug("A different mod information dialog is open. If this is incorrect, please restart MO");
return;
}
std::vector<ModInfo::EFlag> flags = modInfo->getFlags();
@@ -3136,11 +3010,7 @@ void MainWindow::displayModInformation(
} else {
qobject_cast<OverwriteInfoDialog*>(dialog)->setModInfo(modInfo);
}
- QSettings &settings = m_OrganizerCore.settings().directInterface();
- QString key = QString("geometry/%1").arg(dialog->objectName());
- if (settings.contains(key)) {
- dialog->restoreGeometry(settings.value(key).toByteArray());
- }
+
dialog->show();
dialog->raise();
dialog->activateWindow();
@@ -3159,16 +3029,7 @@ void MainWindow::displayModInformation(
dialog.selectTab(tabID);
}
- dialog.restoreState(m_OrganizerCore.settings());
- QSettings &settings = m_OrganizerCore.settings().directInterface();
- QString key = QString("geometry/%1").arg(dialog.objectName());
- if (settings.contains(key)) {
- dialog.restoreGeometry(settings.value(key).toByteArray());
- }
-
dialog.exec();
- dialog.saveState(m_OrganizerCore.settings());
- settings.setValue(key, dialog.saveGeometry());
modInfo->saveMeta();
emit modInfoDisplayed();
@@ -3279,7 +3140,7 @@ void MainWindow::displayModInformation(const QString &modName, ModInfoTabIDs tab
{
unsigned int index = ModInfo::getIndex(modName);
if (index == UINT_MAX) {
- qCritical("failed to resolve mod name %s", qUtf8Printable(modName));
+ log::error("failed to resolve mod name {}", modName);
return;
}
@@ -3364,7 +3225,7 @@ void MainWindow::visitOnNexus_clicked()
if (modID > 0) {
linkClicked(NexusInterface::instance(&m_PluginContainer)->getModURL(modID, gameName));
} else {
- qCritical() << "mod '" << info->name() << "' has no nexus id";
+ log::error("mod '{}' has no nexus id", info->name());
}
}
}
@@ -3420,12 +3281,12 @@ void MainWindow::openExplorer_clicked()
if (selection->hasSelection() && selection->selectedRows().count() > 1) {
for (QModelIndex idx : selection->selectedRows()) {
ModInfo::Ptr info = ModInfo::getByIndex(idx.data(Qt::UserRole + 1).toInt());
- shell::ExploreFile(info->absolutePath());
+ shell::Explore(info->absolutePath());
}
}
else {
ModInfo::Ptr modInfo = ModInfo::getByIndex(m_ContextRow);
- shell::ExploreFile(modInfo->absolutePath());
+ shell::Explore(modInfo->absolutePath());
}
}
@@ -3440,14 +3301,14 @@ void MainWindow::openPluginOriginExplorer_clicked()
continue;
}
ModInfo::Ptr modInfo = ModInfo::getByIndex(modIndex);
- shell::ExploreFile(modInfo->absolutePath());
+ shell::Explore(modInfo->absolutePath());
}
}
else {
QModelIndex idx = selection->currentIndex();
QString fileName = idx.data().toString();
ModInfo::Ptr modInfo = ModInfo::getByIndex(ModInfo::getIndex(m_OrganizerCore.pluginList()->origin(fileName)));
- shell::ExploreFile(modInfo->absolutePath());
+ shell::Explore(modInfo->absolutePath());
}
}
@@ -3462,7 +3323,7 @@ void MainWindow::openExplorer_activated()
std::vector<ModInfo::EFlag> flags = modInfo->getFlags();
if (modInfo->isRegular() || (std::find(flags.begin(), flags.end(), ModInfo::FLAG_OVERWRITE) != flags.end())) {
- shell::ExploreFile(modInfo->absolutePath());
+ shell::Explore(modInfo->absolutePath());
}
}
@@ -3483,7 +3344,7 @@ void MainWindow::openExplorer_activated()
std::vector<ModInfo::EFlag> flags = modInfo->getFlags();
if (modInfo->isRegular() || (std::find(flags.begin(), flags.end(), ModInfo::FLAG_OVERWRITE) != flags.end())) {
- shell::ExploreFile(modInfo->absolutePath());
+ shell::Explore(modInfo->absolutePath());
}
}
}
@@ -3747,32 +3608,37 @@ void MainWindow::createSeparator_clicked()
{
m_OrganizerCore.modList()->changeModPriority(ModInfo::getIndex(name), newPriority);
}
- QSettings &settings = m_OrganizerCore.settings().directInterface();
- QColor previousColor = settings.value("previousSeparatorColor", QColor()).value<QColor>();
- if (previousColor.isValid()) {
- ModInfo::getByIndex(ModInfo::getIndex(name))->setColor(previousColor);
- }
+ if (auto c=m_OrganizerCore.settings().colors().previousSeparatorColor()) {
+ ModInfo::getByIndex(ModInfo::getIndex(name))->setColor(*c);
+ }
}
void MainWindow::setColor_clicked()
{
- QSettings &settings = m_OrganizerCore.settings().directInterface();
+ auto& settings = m_OrganizerCore.settings();
ModInfo::Ptr modInfo = ModInfo::getByIndex(m_ContextRow);
+
QColorDialog dialog(this);
dialog.setOption(QColorDialog::ShowAlphaChannel);
+
QColor currentColor = modInfo->getColor();
- QColor previousColor = settings.value("previousSeparatorColor", QColor()).value<QColor>();
- if (currentColor.isValid())
+ if (currentColor.isValid()) {
dialog.setCurrentColor(currentColor);
- else
- dialog.setCurrentColor(previousColor);
+ }
+ else if (auto c=settings.colors().previousSeparatorColor()) {
+ dialog.setCurrentColor(*c);
+ }
+
if (!dialog.exec())
return;
+
currentColor = dialog.currentColor();
if (!currentColor.isValid())
return;
- settings.setValue("previousSeparatorColor", currentColor);
+
+ settings.colors().setPreviousSeparatorColor(currentColor);
+
QItemSelectionModel *selection = ui->modList->selectionModel();
if (selection->hasSelection() && selection->selectedRows().count() > 1) {
for (QModelIndex idx : selection->selectedRows()) {
@@ -3807,7 +3673,8 @@ void MainWindow::resetColor_clicked()
else {
modInfo->setColor(color);
}
- Settings::instance().directInterface().remove("previousSeparatorColor");
+
+ m_OrganizerCore.settings().colors().removePreviousSeparatorColor();
}
void MainWindow::createModFromOverwrite()
@@ -3853,17 +3720,10 @@ void MainWindow::moveOverwriteContentToExistingMod()
}
ListDialog dialog(this);
- QSettings &settings = m_OrganizerCore.settings().directInterface();
- QString key = QString("geometry/%1").arg(dialog.objectName());
-
dialog.setWindowTitle("Select a mod...");
dialog.setChoices(mods);
- if (settings.contains(key)) {
- dialog.restoreGeometry(settings.value(key).toByteArray());
- }
if (dialog.exec() == QDialog::Accepted) {
-
QString result = dialog.getChoice();
if (!result.isEmpty()) {
@@ -3878,14 +3738,13 @@ void MainWindow::moveOverwriteContentToExistingMod()
}
if (modAbsolutePath.isNull()) {
- qWarning("Mod %s has not been found, for some reason", qUtf8Printable(result));
+ log::warn("Mod {} has not been found, for some reason", result);
return;
}
doMoveOverwriteContentToMod(modAbsolutePath);
}
}
- settings.setValue(key, dialog.saveGeometry());
}
void MainWindow::doMoveOverwriteContentToMod(const QString &modAbsolutePath)
@@ -3902,7 +3761,8 @@ void MainWindow::doMoveOverwriteContentToMod(const QString &modAbsolutePath)
MessageDialog::showMessage(tr("Move successful."), this);
}
else {
- qCritical("Move operation failed: %s", qUtf8Printable(windowsErrorString(::GetLastError())));
+ const auto e = GetLastError();
+ log::error("Move operation failed: {}", formatSystemMessage(e));
}
m_OrganizerCore.refreshModList();
@@ -3931,7 +3791,8 @@ void MainWindow::clearOverwrite()
updateProblemsButton();
m_OrganizerCore.refreshModList();
} else {
- qCritical("Delete operation failed: %s", qUtf8Printable(windowsErrorString(::GetLastError())));
+ const auto e = GetLastError();
+ log::error("Delete operation failed: {}", formatSystemMessage(e));
}
}
}
@@ -3985,7 +3846,7 @@ void MainWindow::on_modList_doubleClicked(const QModelIndex &index)
case ModList::COL_MODID: tab = ModInfoTabIDs::Nexus; break;
case ModList::COL_GAME: tab = ModInfoTabIDs::Nexus; break;
case ModList::COL_CATEGORY: tab = ModInfoTabIDs::Categories; break;
- case ModList::COL_FLAGS: tab = ModInfoTabIDs::Conflicts; break;
+ case ModList::COL_CONFLICTFLAGS: tab = ModInfoTabIDs::Conflicts; break;
}
displayModInformation(sourceIdx.row(), tab);
@@ -4175,7 +4036,7 @@ void MainWindow::addRemoveCategoriesFromMenu(QMenu *menu, int modRow, int refere
void MainWindow::addRemoveCategories_MenuHandler() {
QMenu *menu = qobject_cast<QMenu*>(sender());
if (menu == nullptr) {
- qCritical("not a menu?");
+ log::error("not a menu?");
return;
}
@@ -4189,7 +4050,7 @@ void MainWindow::addRemoveCategories_MenuHandler() {
int maxRow = -1;
for (const QPersistentModelIndex &idx : selected) {
- qDebug("change categories on: %s", qUtf8Printable(idx.data().toString()));
+ log::debug("change categories on: {}", idx.data().toString());
QModelIndex modIdx = mapToModel(m_OrganizerCore.modList(), idx);
if (modIdx.row() != m_ContextIdx.row()) {
addRemoveCategoriesFromMenu(menu, modIdx.row(), m_ContextIdx.row());
@@ -4216,7 +4077,7 @@ void MainWindow::addRemoveCategories_MenuHandler() {
void MainWindow::replaceCategories_MenuHandler() {
QMenu *menu = qobject_cast<QMenu*>(sender());
if (menu == nullptr) {
- qCritical("not a menu?");
+ log::error("not a menu?");
return;
}
@@ -4271,10 +4132,10 @@ void MainWindow::saveArchiveList()
}
}
if (archiveFile.commitIfDifferent(m_ArchiveListHash)) {
- qDebug("%s saved", qUtf8Printable(QDir::toNativeSeparators(m_OrganizerCore.currentProfile()->getArchivesFileName())));
+ log::debug("{} saved", QDir::toNativeSeparators(m_OrganizerCore.currentProfile()->getArchivesFileName()));
}
} else {
- qWarning("archive list not initialised");
+ log::warn("archive list not initialised");
}
}
@@ -4287,11 +4148,11 @@ void MainWindow::checkModsForUpdates()
NexusInterface::instance(&m_PluginContainer)->requestTrackingInfo(this, QVariant(), QString());
} else {
QString apiKey;
- if (m_OrganizerCore.settings().getNexusApiKey(apiKey)) {
+ if (m_OrganizerCore.settings().nexus().apiKey(apiKey)) {
m_OrganizerCore.doAfterLogin([this] () { this->checkModsForUpdates(); });
NexusInterface::instance(&m_PluginContainer)->getAccessManager()->apiCheck(apiKey);
} else {
- qWarning("You are not currently authenticated with Nexus. Please do so under Settings -> Nexus.");
+ log::warn("You are not currently authenticated with Nexus. Please do so under Settings -> Nexus.");
}
}
@@ -4305,13 +4166,17 @@ void MainWindow::checkModsForUpdates()
}
if (updatesAvailable || checkingModsForUpdate) {
- m_ModListSortProxy->setCategoryFilter(boost::assign::list_of(CategoryFactory::CATEGORY_SPECIAL_UPDATEAVAILABLE));
- for (int i = 0; i < ui->categoriesList->topLevelItemCount(); ++i) {
- if (ui->categoriesList->topLevelItem(i)->data(0, Qt::UserRole) == CategoryFactory::CATEGORY_SPECIAL_UPDATEAVAILABLE) {
- ui->categoriesList->setCurrentItem(ui->categoriesList->topLevelItem(i));
- break;
- }
- }
+ m_ModListSortProxy->setCriteria({{
+ ModListSortProxy::TypeSpecial,
+ CategoryFactory::UpdateAvailable,
+ false}
+ });
+
+ m_Filters->setSelection({{
+ ModListSortProxy::TypeSpecial,
+ CategoryFactory::UpdateAvailable,
+ false
+ }});
}
}
@@ -4411,7 +4276,7 @@ void MainWindow::addPrimaryCategoryCandidates(QMenu *primaryCategoryMenu,
categoryBox->setChecked(categoryID == info->getPrimaryCategory());
action->setDefaultWidget(categoryBox);
} catch (const std::exception &e) {
- qCritical("failed to create category checkbox: %s", e.what());
+ log::error("failed to create category checkbox: {}", e.what());
}
action->setData(categoryID);
@@ -4423,7 +4288,7 @@ void MainWindow::addPrimaryCategoryCandidates()
{
QMenu *menu = qobject_cast<QMenu*>(sender());
if (menu == nullptr) {
- qCritical("not a menu?");
+ log::error("not a menu?");
return;
}
menu->clear();
@@ -4451,61 +4316,66 @@ void MainWindow::disableVisibleMods()
void MainWindow::openInstanceFolder()
{
QString dataPath = qApp->property("dataPath").toString();
- shell::ExploreFile(dataPath);
+ shell::Explore(dataPath);
}
void MainWindow::openLogsFolder()
{
QString logsPath = qApp->property("dataPath").toString() + "/" + QString::fromStdWString(AppConfig::logPath());
- shell::ExploreFile(logsPath);
+ shell::Explore(logsPath);
}
void MainWindow::openInstallFolder()
{
- shell::ExploreFile(qApp->applicationDirPath());
+ shell::Explore(qApp->applicationDirPath());
}
void MainWindow::openPluginsFolder()
{
QString pluginsPath = QCoreApplication::applicationDirPath() + "/" + ToQString(AppConfig::pluginPath());
- shell::ExploreFile(pluginsPath);
+ shell::Explore(pluginsPath);
}
+void MainWindow::openStylesheetsFolder()
+{
+ QString ssPath = QCoreApplication::applicationDirPath() + "/" + ToQString(AppConfig::stylesheetsPath());
+ shell::Explore(ssPath);
+}
void MainWindow::openProfileFolder()
{
- shell::ExploreFile(m_OrganizerCore.currentProfile()->absolutePath());
+ shell::Explore(m_OrganizerCore.currentProfile()->absolutePath());
}
void MainWindow::openIniFolder()
{
if (m_OrganizerCore.currentProfile()->localSettingsEnabled())
{
- shell::ExploreFile(m_OrganizerCore.currentProfile()->absolutePath());
+ shell::Explore(m_OrganizerCore.currentProfile()->absolutePath());
}
else {
- shell::ExploreFile(m_OrganizerCore.managedGame()->documentsDirectory());
+ shell::Explore(m_OrganizerCore.managedGame()->documentsDirectory());
}
}
void MainWindow::openDownloadsFolder()
{
- shell::ExploreFile(m_OrganizerCore.settings().getDownloadDirectory());
+ shell::Explore(m_OrganizerCore.settings().paths().downloads());
}
void MainWindow::openModsFolder()
{
- shell::ExploreFile(m_OrganizerCore.settings().getModDirectory());
+ shell::Explore(m_OrganizerCore.settings().paths().mods());
}
void MainWindow::openGameFolder()
{
- shell::ExploreFile(m_OrganizerCore.managedGame()->gameDirectory());
+ shell::Explore(m_OrganizerCore.managedGame()->gameDirectory());
}
void MainWindow::openMyGamesFolder()
{
- shell::ExploreFile(m_OrganizerCore.managedGame()->documentsDirectory());
+ shell::Explore(m_OrganizerCore.managedGame()->documentsDirectory());
}
@@ -4688,33 +4558,29 @@ static void addMenuAsPushButton(QMenu *menu, QMenu *subMenu)
QMenu *MainWindow::openFolderMenu()
{
-
QMenu *FolderMenu = new QMenu(this);
+ // game folders that are not necessarily MO-specific
FolderMenu->addAction(tr("Open Game folder"), this, SLOT(openGameFolder()));
-
FolderMenu->addAction(tr("Open MyGames folder"), this, SLOT(openMyGamesFolder()));
-
FolderMenu->addAction(tr("Open INIs folder"), this, SLOT(openIniFolder()));
FolderMenu->addSeparator();
+ // MO-specific folders that are related to modding the game
FolderMenu->addAction(tr("Open Instance folder"), this, SLOT(openInstanceFolder()));
-
FolderMenu->addAction(tr("Open Mods folder"), this, SLOT(openModsFolder()));
-
FolderMenu->addAction(tr("Open Profile folder"), this, SLOT(openProfileFolder()));
-
FolderMenu->addAction(tr("Open Downloads folder"), this, SLOT(openDownloadsFolder()));
FolderMenu->addSeparator();
+ // MO-specific folders that are not directly related to modding and are either
+ // in the installation folder or the instance
FolderMenu->addAction(tr("Open MO2 Install folder"), this, SLOT(openInstallFolder()));
-
FolderMenu->addAction(tr("Open MO2 Plugins folder"), this, SLOT(openPluginsFolder()));
-
- FolderMenu->addAction(tr("Open MO2 Logs folder"), this, SLOT(openLogsFolder()));
-
+ FolderMenu->addAction(tr("Open MO2 Stylesheets folder"), this, SLOT(openStylesheetsFolder()));
+ FolderMenu->addAction(tr("Open MO2 Logs folder"), this, SLOT(openLogsFolder()));
return FolderMenu;
}
@@ -4861,7 +4727,7 @@ void MainWindow::on_modList_customContextMenuRequested(const QPoint &pos)
menu.addSeparator();
- if (info->getNexusID() > 0 && Settings::instance().endorsementIntegration()) {
+ if (info->getNexusID() > 0 && Settings::instance().nexus().endorsementIntegration()) {
switch (info->endorsedState()) {
case ModInfo::ENDORSED_TRUE: {
menu.addAction(tr("Un-Endorse"), this, SLOT(unendorse_clicked()));
@@ -4937,40 +4803,6 @@ void MainWindow::on_modList_customContextMenuRequested(const QPoint &pos)
}
-void MainWindow::on_categoriesList_itemSelectionChanged()
-{
- QModelIndexList indices = ui->categoriesList->selectionModel()->selectedRows();
- std::vector<int> categories;
- std::vector<int> content;
- for (const QModelIndex &index : indices) {
- int filterType = index.data(Qt::UserRole + 1).toInt();
- if ((filterType == ModListSortProxy::TYPE_CATEGORY)
- || (filterType == ModListSortProxy::TYPE_SPECIAL)) {
- int categoryId = index.data(Qt::UserRole).toInt();
- if (categoryId != CategoryFactory::CATEGORY_NONE) {
- categories.push_back(categoryId);
- }
- } else if (filterType == ModListSortProxy::TYPE_CONTENT) {
- int contentId = index.data(Qt::UserRole).toInt();
- content.push_back(contentId);
- }
- }
-
- m_ModListSortProxy->setCategoryFilter(categories);
- m_ModListSortProxy->setContentFilter(content);
- ui->clickBlankButton->setEnabled(categories.size() > 0 || content.size() >0);
-
- if (indices.count() == 0) {
- ui->currentCategoryLabel->setText(QString("(%1)").arg(tr("<All>")));
- } else if (indices.count() > 1) {
- ui->currentCategoryLabel->setText(QString("(%1)").arg(tr("<Multiple>")));
- } else {
- ui->currentCategoryLabel->setText(QString("(%1)").arg(indices.first().data().toString()));
- }
- ui->modList->reset();
-}
-
-
void MainWindow::deleteSavegame_clicked()
{
SaveGameInfo const *info = m_OrganizerCore.managedGame()->feature<SaveGameInfo>();
@@ -5071,33 +4903,43 @@ void MainWindow::on_savegameList_customContextMenuRequested(const QPoint &pos)
void MainWindow::linkToolbar()
{
- Executable& exe = getSelectedExecutable();
+ Executable* exe = getSelectedExecutable();
+ if (!exe) {
+ return;
+ }
- exe.setShownOnToolbar(!exe.isShownOnToolbar());
+ exe->setShownOnToolbar(!exe->isShownOnToolbar());
updatePinnedExecutables();
}
void MainWindow::linkDesktop()
{
- env::Shortcut(getSelectedExecutable()).toggle(env::Shortcut::Desktop);
+ if (auto* exe=getSelectedExecutable()) {
+ env::Shortcut(*exe).toggle(env::Shortcut::Desktop);
+ }
}
void MainWindow::linkMenu()
{
- env::Shortcut(getSelectedExecutable()).toggle(env::Shortcut::StartMenu);
+ if (auto* exe=getSelectedExecutable()) {
+ env::Shortcut(*exe).toggle(env::Shortcut::StartMenu);
+ }
}
void MainWindow::on_linkButton_pressed()
{
- const Executable& exe = getSelectedExecutable();
+ const Executable* exe = getSelectedExecutable();
+ if (!exe) {
+ return;
+ }
const QIcon addIcon(":/MO/gui/link");
const QIcon removeIcon(":/MO/gui/remove");
- env::Shortcut shortcut(exe);
+ env::Shortcut shortcut(*exe);
m_LinkToolbar->setIcon(
- exe.isShownOnToolbar() ? removeIcon : addIcon);
+ exe->isShownOnToolbar() ? removeIcon : addIcon);
m_LinkDesktop->setIcon(
shortcut.exists(env::Shortcut::Desktop) ? removeIcon : addIcon);
@@ -5110,49 +4952,64 @@ void MainWindow::on_actionSettings_triggered()
{
Settings &settings = m_OrganizerCore.settings();
- QString oldModDirectory(settings.getModDirectory());
- QString oldCacheDirectory(settings.getCacheDirectory());
- QString oldProfilesDirectory(settings.getProfileDirectory());
- QString oldManagedGameDirectory(settings.getManagedGameDirectory());
- bool oldDisplayForeign(settings.displayForeign());
- bool proxy = settings.useProxy();
+ QString oldModDirectory(settings.paths().mods());
+ QString oldCacheDirectory(settings.paths().cache());
+ QString oldProfilesDirectory(settings.paths().profiles());
+ QString oldManagedGameDirectory(settings.game().directory().value_or(""));
+ bool oldDisplayForeign(settings.interface().displayForeign());
+ bool proxy = settings.network().useProxy();
DownloadManager *dlManager = m_OrganizerCore.downloadManager();
+ const bool oldCheckForUpdates = settings.checkForUpdates();
+ const int oldMaxDumps = settings.diagnostics().crashDumpsMax();
+
- settings.query(&m_PluginContainer, this);
+ SettingsDialog dialog(&m_PluginContainer, settings, this);
+ dialog.exec();
+
+ auto e = dialog.exitNeeded();
- if (oldManagedGameDirectory != settings.getManagedGameDirectory()) {
- QMessageBox::about(this, tr("Restarting MO"),
- tr("Changing the managed game directory requires restarting MO.\n"
- "Any pending downloads will be paused.\n\n"
- "Click OK to restart MO now."));
- dlManager->pauseAll();
- qApp->exit(INT_MAX);
+ if (oldManagedGameDirectory != settings.game().directory()) {
+ e |= Exit::Restart;
+ }
+
+ if (e.testFlag(Exit::Restart)) {
+ const auto r = MOBase::TaskDialog(this)
+ .title(tr("Restart Mod Organizer"))
+ .main("Restart Mod Organizer")
+ .content(tr("Mod Organizer must restart to finish configuration changes"))
+ .icon(QMessageBox::Question)
+ .button({tr("Restart"), QMessageBox::Yes})
+ .button({tr("Continue"), tr("Some things might be weird."), QMessageBox::No})
+ .exec();
+
+ if (r == QMessageBox::Yes) {
+ ExitModOrganizer(e);
+ }
}
InstallationManager *instManager = m_OrganizerCore.installationManager();
- instManager->setModsDirectory(settings.getModDirectory());
- instManager->setDownloadDirectory(settings.getDownloadDirectory());
+ instManager->setModsDirectory(settings.paths().mods());
+ instManager->setDownloadDirectory(settings.paths().downloads());
fixCategories();
refreshFilters();
- if (settings.getProfileDirectory() != oldProfilesDirectory) {
+ if (settings.paths().profiles() != oldProfilesDirectory) {
refreshProfiles();
}
- if (dlManager->getOutputDirectory() != settings.getDownloadDirectory()) {
+ if (dlManager->getOutputDirectory() != settings.paths().downloads()) {
if (dlManager->downloadsInProgress()) {
MessageDialog::showMessage(tr("Can't change download directory while "
"downloads are in progress!"),
this);
} else {
- dlManager->setOutputDirectory(settings.getDownloadDirectory());
+ dlManager->setOutputDirectory(settings.paths().downloads());
}
}
- dlManager->setPreferredServers(settings.getPreferredServers());
- if ((settings.getModDirectory() != oldModDirectory)
- || (settings.displayForeign() != oldDisplayForeign)) {
+ if ((settings.paths().mods() != oldModDirectory)
+ || (settings.interface().displayForeign() != oldDisplayForeign)) {
m_OrganizerCore.profileRefresh();
}
@@ -5177,23 +5034,34 @@ void MainWindow::on_actionSettings_triggered()
m_OrganizerCore.refreshLists();
}
- if (settings.getCacheDirectory() != oldCacheDirectory) {
- NexusInterface::instance(&m_PluginContainer)->setCacheDirectory(settings.getCacheDirectory());
+ if (settings.paths().cache() != oldCacheDirectory) {
+ NexusInterface::instance(&m_PluginContainer)->setCacheDirectory(
+ settings.paths().cache());
}
- if (proxy != settings.useProxy()) {
- activateProxy(settings.useProxy());
+ if (proxy != settings.network().useProxy()) {
+ activateProxy(settings.network().useProxy());
}
- m_statusBar->checkSettings(m_OrganizerCore.settings());
+ ui->statusBar->checkSettings(m_OrganizerCore.settings());
updateDownloadView();
- m_OrganizerCore.updateVFSParams(settings.logLevel(), settings.crashDumpsType(), settings.executablesBlacklist());
- m_OrganizerCore.cycleDiagnostics();
+ m_OrganizerCore.setLogLevel(settings.diagnostics().logLevel());
+
+ if (settings.diagnostics().crashDumpsMax() != oldMaxDumps) {
+ m_OrganizerCore.cycleDiagnostics();
+ }
toggleMO2EndorseState();
-}
+ if (oldCheckForUpdates != settings.checkForUpdates()) {
+ toggleUpdateAction();
+
+ if (settings.checkForUpdates()) {
+ m_OrganizerCore.checkForUpdates();
+ }
+ }
+}
void MainWindow::on_actionNexus_triggered()
{
@@ -5217,7 +5085,7 @@ void MainWindow::installTranslator(const QString &name)
QString fileName = name + "_" + m_CurrentLanguage;
if (!translator->load(fileName, qApp->applicationDirPath() + "/translations")) {
if (m_CurrentLanguage.contains(QRegularExpression("^.*_(EN|en)(-.*)?$"))) {
- qDebug("localization file %s not found", qUtf8Printable(fileName));
+ log::debug("localization file %s not found", fileName);
} // we don't actually expect localization files for English (en, en-us, en-uk, and any variation thereof)
}
@@ -5242,7 +5110,7 @@ void MainWindow::languageChange(const QString &newLanguage)
installTranslator(QFileInfo(fileName).baseName());
}
ui->retranslateUi(this);
- qDebug("loaded language %s", qUtf8Printable(newLanguage));
+ log::debug("loaded language {}", newLanguage);
ui->profileBox->setItemText(0, QObject::tr("<Manage...>"));
@@ -5305,43 +5173,42 @@ void MainWindow::addAsExecutable()
return;
}
- using FileExecutionTypes = OrganizerCore::FileExecutionTypes;
-
- QFileInfo targetInfo(m_ContextItem->data(0, Qt::UserRole).toString());
- QFileInfo binaryInfo;
- QString arguments;
- FileExecutionTypes type;
+ const QFileInfo target(m_ContextItem->data(0, Qt::UserRole).toString());
+ const auto fec = spawn::getFileExecutionContext(this, target);
- if (!OrganizerCore::getFileExecutionContext(this, targetInfo, binaryInfo, arguments, type)) {
- return;
- }
-
- switch (type)
+ switch (fec.type)
{
- case FileExecutionTypes::Executable: {
- QString name = QInputDialog::getText(this, tr("Enter Name"),
- tr("Please enter a name for the executable"), QLineEdit::Normal,
- targetInfo.baseName());
-
- if (!name.isEmpty()) {
- //Note: If this already exists, you'll lose custom settings
- m_OrganizerCore.executablesList()->setExecutable(Executable()
- .title(name)
- .binaryInfo(binaryInfo)
- .arguments(arguments)
- .workingDirectory(targetInfo.absolutePath()));
+ case spawn::FileExecutionTypes::Executable:
+ {
+ const QString name = QInputDialog::getText(
+ this, tr("Enter Name"),
+ tr("Enter a name for the executable"),
+ QLineEdit::Normal,
+ target.completeBaseName());
- refreshExecutablesList();
- }
+ if (!name.isEmpty()) {
+ //Note: If this already exists, you'll lose custom settings
+ m_OrganizerCore.executablesList()->setExecutable(Executable()
+ .title(name)
+ .binaryInfo(fec.binary)
+ .arguments(fec.arguments)
+ .workingDirectory(target.absolutePath()));
- break;
+ refreshExecutablesList();
}
- case FileExecutionTypes::Other: // fall-through
- default: {
- QMessageBox::information(this, tr("Not an executable"), tr("This is not a recognized executable."));
- break;
- }
+ break;
+ }
+
+ case spawn::FileExecutionTypes::Other: // fall-through
+ default:
+ {
+ QMessageBox::information(
+ this, tr("Not an executable"),
+ tr("This is not a recognized executable."));
+
+ break;
+ }
}
}
@@ -5456,10 +5323,29 @@ void MainWindow::disableSelectedMods_clicked()
}
-void MainWindow::previewDataFile()
+void MainWindow::activateDataTreeItem(QTreeWidgetItem *item, int column)
{
- QString fileName = QDir::fromNativeSeparators(m_ContextItem->data(0, Qt::UserRole).toString());
- m_OrganizerCore.previewFileWithAlternatives(this, fileName);
+ const auto isArchive = item->data(0, Qt::UserRole + 1).toBool();
+ const auto isDirectory = item->data(0, Qt::UserRole + 3).toBool();
+
+ if (isArchive || isDirectory) {
+ return;
+ }
+
+ const QString path = item->data(0, Qt::UserRole).toString();
+ if (path.isEmpty()) {
+ return;
+ }
+
+ const QFileInfo targetInfo(path);
+
+ const auto tryPreview = m_OrganizerCore.settings().interface().doubleClicksOpenPreviews();
+
+ if (tryPreview && m_PluginContainer.previewGenerator().previewSupported(targetInfo.suffix())) {
+ previewDataFile(item);
+ } else {
+ openDataFile(item);
+ }
}
void MainWindow::openDataFile()
@@ -5468,8 +5354,69 @@ void MainWindow::openDataFile()
return;
}
- QFileInfo targetInfo(m_ContextItem->data(0, Qt::UserRole).toString());
- m_OrganizerCore.executeFileVirtualized(this, targetInfo);
+ openDataFile(m_ContextItem);
+}
+
+void MainWindow::openDataFile(QTreeWidgetItem* item)
+{
+ const auto isArchive = item->data(0, Qt::UserRole + 1).toBool();
+ const auto isDirectory = item->data(0, Qt::UserRole + 3).toBool();
+
+ if (isArchive || isDirectory) {
+ return;
+ }
+
+ const QString path = item->data(0, Qt::UserRole).toString();
+ const QFileInfo targetInfo(path);
+
+ m_OrganizerCore.processRunner()
+ .setFromFile(this, targetInfo)
+ .setHooked(false)
+ .setWaitForCompletion(ProcessRunner::Refresh)
+ .run();
+}
+
+void MainWindow::runDataFileHooked()
+{
+ if (m_ContextItem == nullptr) {
+ return;
+ }
+
+ runDataFileHooked(m_ContextItem);
+}
+
+void MainWindow::runDataFileHooked(QTreeWidgetItem* item)
+{
+ const auto isArchive = item->data(0, Qt::UserRole + 1).toBool();
+ const auto isDirectory = item->data(0, Qt::UserRole + 3).toBool();
+
+ if (isArchive || isDirectory) {
+ return;
+ }
+
+ const QString path = item->data(0, Qt::UserRole).toString();
+ const QFileInfo targetInfo(path);
+
+ m_OrganizerCore.processRunner()
+ .setFromFile(this, targetInfo)
+ .setHooked(true)
+ .setWaitForCompletion(ProcessRunner::Refresh)
+ .run();
+}
+
+void MainWindow::previewDataFile()
+{
+ if (m_ContextItem == nullptr) {
+ return;
+ }
+
+ previewDataFile(m_ContextItem);
+}
+
+void MainWindow::previewDataFile(QTreeWidgetItem* item)
+{
+ QString fileName = QDir::fromNativeSeparators(item->data(0, Qt::UserRole).toString());
+ m_OrganizerCore.previewFileWithAlternatives(this, fileName);
}
void MainWindow::openDataOriginExplorer_clicked()
@@ -5487,15 +5434,42 @@ void MainWindow::openDataOriginExplorer_clicked()
const auto fullPath = m_ContextItem->data(0, Qt::UserRole).toString();
- qDebug().nospace() << "opening in explorer: " << fullPath;
- shell::ExploreFile(fullPath);
+ log::debug("opening in explorer: {}", fullPath);
+ shell::Explore(fullPath);
+}
+
+void MainWindow::openDataModInfo_clicked()
+{
+ if (m_ContextItem == nullptr) {
+ return;
+ }
+
+ const auto originID = m_ContextItem->data(1, Qt::UserRole + 1).toInt();
+ if (originID == 0) {
+ // unmanaged
+ return;
+ }
+
+ const auto& origin = m_OrganizerCore.directoryStructure()->getOriginByID(originID);
+ const auto& name = QString::fromStdWString(origin.getName());
+
+ unsigned int index = ModInfo::getIndex(name);
+ if (index == UINT_MAX) {
+ log::error("can't open mod info, mod '{}' not found", name);
+ return;
+ }
+
+ ModInfo::Ptr modInfo = ModInfo::getByIndex(index);
+ if (modInfo) {
+ displayModInformation(modInfo, index, ModInfoTabIDs::None);
+ }
}
void MainWindow::updateAvailable()
{
ui->actionUpdate->setEnabled(true);
ui->actionUpdate->setToolTip(tr("Update available"));
- m_statusBar->setUpdateAvailable(true);
+ ui->statusBar->setUpdateAvailable(true);
}
@@ -5505,37 +5479,85 @@ void MainWindow::motdReceived(const QString &motd)
// internet connection is faster next time
if (m_StartTime.secsTo(QTime::currentTime()) < 5) {
uint hash = qHash(motd);
- if (hash != m_OrganizerCore.settings().getMotDHash()) {
+ if (hash != m_OrganizerCore.settings().motdHash()) {
MotDDialog dialog(motd);
dialog.exec();
- m_OrganizerCore.settings().setMotDHash(hash);
+ m_OrganizerCore.settings().setMotdHash(hash);
}
}
}
void MainWindow::on_dataTree_customContextMenuRequested(const QPoint &pos)
{
- QTreeWidget *dataTree = findChild<QTreeWidget*>("dataTree");
- m_ContextItem = dataTree->itemAt(pos.x(), pos.y());
+ m_ContextItem = ui->dataTree->itemAt(pos.x(), pos.y());
QMenu menu;
if ((m_ContextItem != nullptr) && (m_ContextItem->childCount() == 0)
&& (m_ContextItem->data(0, Qt::UserRole + 3).toBool() != true)) {
- menu.addAction(tr("Open/Execute"), this, SLOT(openDataFile()));
- menu.addAction(tr("Add as Executable"), this, SLOT(addAsExecutable()));
-
QString fileName = m_ContextItem->text(0);
+ const auto isArchive = m_ContextItem->data(0, Qt::UserRole + 1).toBool();
+ const auto isDirectory = m_ContextItem->data(0, Qt::UserRole + 3).toBool();
+
+ QAction* open = nullptr;
+ QAction* runHooked = nullptr;
+ QAction* preview = nullptr;
+
+ if (canRunFile(isArchive, fileName)) {
+ open = new QAction(tr("&Execute"), ui->dataTree);
+ runHooked = new QAction(tr("Execute with &VFS"), ui->dataTree);
+ } else if (canOpenFile(isArchive, fileName)) {
+ open = new QAction(tr("&Open"), ui->dataTree);
+ runHooked = new QAction(tr("Open with &VFS"), ui->dataTree);
+ }
+
if (m_PluginContainer.previewGenerator().previewSupported(QFileInfo(fileName).suffix())) {
- menu.addAction(tr("Preview"), this, SLOT(previewDataFile()));
+ preview = new QAction(tr("Preview"), ui->dataTree);
}
- const auto isArchive = m_ContextItem->data(0, Qt::UserRole + 1).toBool();
- const auto isDirectory = m_ContextItem->data(0, Qt::UserRole + 3).toBool();
+ if (open) {
+ connect(open, &QAction::triggered, [&]{ openDataFile(); });
+ }
+
+ if (runHooked) {
+ connect(runHooked, &QAction::triggered, [&]{ runDataFileHooked(); });
+ }
+
+ if (preview) {
+ connect(preview, &QAction::triggered, [&]{ previewDataFile(); });
+ }
+
+ if (open && preview) {
+ if (m_OrganizerCore.settings().interface().doubleClicksOpenPreviews()) {
+ menu.addAction(preview);
+ menu.addAction(open);
+ } else {
+ menu.addAction(open);
+ menu.addAction(preview);
+ }
+ } else {
+ if (open) {
+ menu.addAction(open);
+ }
+
+ if (preview) {
+ menu.addAction(preview);
+ }
+ }
+
+ if (runHooked) {
+ menu.addAction(runHooked);
+ }
+
+ menu.addAction(tr("&Add as Executable"), this, SLOT(addAsExecutable()));
if (!isArchive && !isDirectory) {
menu.addAction("Open Origin in Explorer", this, SLOT(openDataOriginExplorer_clicked()));
}
+ menu.addAction("Open Mod Info", this, SLOT(openDataModInfo_clicked()));
+
+ menu.addSeparator();
+
// offer to hide/unhide file, but not for files from archives
if (!isArchive) {
if (m_ContextItem->text(0).endsWith(ModInfo::s_HiddenExt)) {
@@ -5545,12 +5567,19 @@ void MainWindow::on_dataTree_customContextMenuRequested(const QPoint &pos)
}
}
- menu.addSeparator();
+ if (open || preview || runHooked) {
+ // bold the first option
+ auto* top = menu.actions()[0];
+ auto f = top->font();
+ f.setBold(true);
+ top->setFont(f);
+ }
}
+
menu.addAction(tr("Write To File..."), this, SLOT(writeDataToFile()));
menu.addAction(tr("Refresh"), this, SLOT(on_btnRefreshData_clicked()));
- menu.exec(dataTree->viewport()->mapToGlobal(pos));
+ menu.exec(ui->dataTree->viewport()->mapToGlobal(pos));
}
void MainWindow::on_conflictsCheckBox_toggled(bool)
@@ -5565,9 +5594,7 @@ void MainWindow::on_actionUpdate_triggered()
void MainWindow::on_actionExit_triggered()
{
- if (confirmExit()) {
- qApp->exit();
- }
+ ExitModOrganizer();
}
void MainWindow::actionEndorseMO()
@@ -5631,7 +5658,7 @@ void MainWindow::initDownloadView()
void MainWindow::updateDownloadView()
{
// set the view attribute and default row sizes
- if (m_OrganizerCore.settings().compactDownloads()) {
+ if (m_OrganizerCore.settings().interface().compactDownloads()) {
ui->downloadView->setProperty("downloadView", "compact");
setStyleSheet("DownloadListWidget::item { padding: 4px 2px; }");
} else {
@@ -5644,7 +5671,7 @@ void MainWindow::updateDownloadView()
// reapply global stylesheet on the widget level (!) to override the defaults
//ui->downloadView->setStyleSheet(styleSheet());
- ui->downloadView->setMetaDisplay(m_OrganizerCore.settings().metaDownloads());
+ ui->downloadView->setMetaDisplay(m_OrganizerCore.settings().interface().metaDownloads());
ui->downloadView->style()->unpolish(ui->downloadView);
ui->downloadView->style()->polish(ui->downloadView);
qobject_cast<DownloadListHeader*>(ui->downloadView->header())->customResizeSections();
@@ -5657,32 +5684,58 @@ void MainWindow::modUpdateCheck(std::multimap<QString, int> IDs)
ModInfo::manualUpdateCheck(&m_PluginContainer, this, IDs);
} else {
QString apiKey;
- if (m_OrganizerCore.settings().getNexusApiKey(apiKey)) {
+ if (m_OrganizerCore.settings().nexus().apiKey(apiKey)) {
m_OrganizerCore.doAfterLogin([=]() { this->modUpdateCheck(IDs); });
NexusInterface::instance(&m_PluginContainer)->getAccessManager()->apiCheck(apiKey);
} else
- qWarning("You are not currently authenticated with Nexus. Please do so under Settings -> Nexus.");
+ log::warn("You are not currently authenticated with Nexus. Please do so under Settings -> Nexus.");
}
}
void MainWindow::toggleMO2EndorseState()
{
- if (Settings::instance().endorsementIntegration()) {
- ui->actionEndorseMO->setVisible(true);
- if (Settings::instance().directInterface().contains("endorse_state")) {
- ui->actionEndorseMO->menu()->setEnabled(false);
- if (Settings::instance().directInterface().value("endorse_state").toString() == "Endorsed") {
- ui->actionEndorseMO->setToolTip(tr("Thank you for endorsing MO2! :)"));
- ui->actionEndorseMO->setStatusTip(tr("Thank you for endorsing MO2! :)"));
- } else if (Settings::instance().directInterface().value("endorse_state").toString() == "Abstained") {
- ui->actionEndorseMO->setToolTip(tr("Please reconsider endorsing MO2 on Nexus!"));
- ui->actionEndorseMO->setStatusTip(tr("Please reconsider endorsing MO2 on Nexus!"));
- }
- } else {
- ui->actionEndorseMO->menu()->setEnabled(true);
- }
- } else
+ const auto& s = m_OrganizerCore.settings();
+
+ if (!s.nexus().endorsementIntegration()) {
ui->actionEndorseMO->setVisible(false);
+ return;
+ }
+
+ ui->actionEndorseMO->setVisible(true);
+
+ bool enabled = false;
+ QString text;
+
+ switch (s.nexus().endorsementState())
+ {
+ case EndorsementState::Accepted:
+ {
+ text = tr("Thank you for endorsing MO2! :)");
+ break;
+ }
+
+ case EndorsementState::Refused:
+ {
+ text = tr("Please reconsider endorsing MO2 on Nexus!");
+ break;
+ }
+
+ case EndorsementState::NoDecision:
+ {
+ enabled = true;
+ break;
+ }
+ }
+
+ ui->actionEndorseMO->menu()->setEnabled(enabled);
+ ui->actionEndorseMO->setToolTip(text);
+ ui->actionEndorseMO->setStatusTip(text);
+}
+
+void MainWindow::toggleUpdateAction()
+{
+ const auto& s = m_OrganizerCore.settings();
+ ui->actionUpdate->setVisible(s.checkForUpdates());
}
void MainWindow::nxmEndorsementsAvailable(QVariant userData, QVariant resultData, int)
@@ -5714,22 +5767,26 @@ void MainWindow::nxmEndorsementsAvailable(QVariant userData, QVariant resultData
mod->setIsEndorsed(false);
}
- if (Settings::instance().endorsementIntegration()) {
+ if (Settings::instance().nexus().endorsementIntegration()) {
if (result->first == "skyrimspecialedition" && result->second.first == gamePlugin->nexusModOrganizerID()) {
- Settings::instance().directInterface().setValue("endorse_state", result->second.second);
+ m_OrganizerCore.settings().nexus().setEndorsementState(
+ endorsementStateFromString(result->second.second));
+
toggleMO2EndorseState();
}
}
}
}
- if (!searchedMO2NexusGame && Settings::instance().endorsementIntegration()) {
+ if (!searchedMO2NexusGame && Settings::instance().nexus().endorsementIntegration()) {
auto gamePlugin = m_OrganizerCore.getGame("SkyrimSE");
if (gamePlugin) {
auto iter = sorted.equal_range(gamePlugin->gameNexusName());
for (auto result = iter.first; result != iter.second; ++result) {
if (result->second.first == gamePlugin->nexusModOrganizerID()) {
- Settings::instance().directInterface().setValue("endorse_state", result->second.second);
+ m_OrganizerCore.settings().nexus().setEndorsementState(
+ endorsementStateFromString(result->second.second));
+
toggleMO2EndorseState();
break;
}
@@ -5766,7 +5823,7 @@ void MainWindow::finishUpdateInfo()
auto finalMods = watcher->result();
if (finalMods.empty()) {
- qInfo("None of your mods appear to have had recent file updates.");
+ log::info("None of your mods appear to have had recent file updates.");
}
std::set<std::pair<QString, int>> organizedGames;
@@ -5777,7 +5834,7 @@ void MainWindow::finishUpdateInfo()
}
if (!finalMods.empty() && organizedGames.empty())
- qWarning("All of your mods have been checked recently. We restrict update checks to help preserve your available API requests.");
+ log::warn("All of your mods have been checked recently. We restrict update checks to help preserve your available API requests.");
for (auto game : organizedGames)
NexusInterface::instance(&m_PluginContainer)->requestUpdates(game.second, this, QVariant(), game.first, QString());
@@ -5909,18 +5966,44 @@ void MainWindow::nxmModInfoAvailable(QString gameName, int modID, QVariant userD
void MainWindow::nxmEndorsementToggled(QString, int, QVariant, QVariant resultData, int)
{
- QMap results = resultData.toMap();
- if (results["status"].toString().compare("Endorsed") == 0) {
- QMessageBox::information(this, tr("Thank you!"), tr("Thank you for your endorsement!"));
- Settings::instance().directInterface().setValue("endorse_state", "Endorsed");
- } else if (results["status"].toString().compare("Abstained") == 0) {
- QMessageBox::information(this, tr("Okay."), tr("This mod will not be endorsed and will no longer ask you to endorse."));
- Settings::instance().directInterface().setValue("endorse_state", "Abstained");
+ const QMap results = resultData.toMap();
+
+ auto itor = results.find("status");
+ if (itor == results.end()) {
+ log::error("endorsement response has no status");
+ return;
}
+
+ const auto s = endorsementStateFromString(itor->toString());
+
+ switch (s)
+ {
+ case EndorsementState::Accepted:
+ {
+ QMessageBox::information(this, tr("Thank you!"), tr("Thank you for your endorsement!"));
+ break;
+ }
+
+ case EndorsementState::Refused:
+ {
+ // don't spam message boxes if the user doesn't want to endorse
+ log::info("Mod Organizer will not be endorsed and will no longer ask you to endorse.");
+ break;
+ }
+
+ case EndorsementState::NoDecision:
+ {
+ log::error("bad status '{}' in endorsement response", itor->toString());
+ return;
+ }
+ }
+
+ m_OrganizerCore.settings().nexus().setEndorsementState(s);
toggleMO2EndorseState();
+
if (!disconnect(sender(), SIGNAL(nxmEndorsementToggled(QString, int, QVariant, QVariant, int)),
this, SLOT(nxmEndorsementToggled(QString, int, QVariant, QVariant, int)))) {
- qCritical("failed to disconnect endorsement slot");
+ log::error("failed to disconnect endorsement slot");
}
}
@@ -5954,26 +6037,42 @@ void MainWindow::nxmTrackedModsAvailable(QVariant userData, QVariant resultData,
void MainWindow::nxmDownloadURLs(QString, int, int, QVariant, QVariant resultData, int)
{
- QVariantList serverList = resultData.toList();
+ auto servers = m_OrganizerCore.settings().network().servers();
+
+ for (const QVariant &var : resultData.toList()) {
+ const QVariantMap map = var.toMap();
+
+ const auto name = map["short_name"].toString();
+ const auto isPremium = map["name"].toString().contains("Premium", Qt::CaseInsensitive);
+ const auto isCDN = map["short_name"].toString().contains("CDN", Qt::CaseInsensitive);
- QList<ServerInfo> servers;
- for (const QVariant &server : serverList) {
- QVariantMap serverInfo = server.toMap();
- ServerInfo info;
- info.name = serverInfo["short_name"].toString();
- info.premium = serverInfo["name"].toString().contains("Premium", Qt::CaseInsensitive);
- info.lastSeen = QDate::currentDate();
- info.preferred = serverInfo["short_name"].toString().contains("CDN", Qt::CaseInsensitive);
- servers.append(info);
+ bool found = false;
+
+ for (auto& server : servers) {
+ if (server.name() == name) {
+ // already exists, update
+ server.setPremium(isPremium);
+ server.updateLastSeen();
+ found = true;
+ break;
+ }
+ }
+
+ if (!found) {
+ // new server
+ ServerInfo server(name, isPremium, QDate::currentDate(), isCDN ? 1 : 0, {});
+ servers.add(std::move(server));
+ }
}
- m_OrganizerCore.settings().updateServers(servers);
+
+ m_OrganizerCore.settings().network().updateServers(servers);
}
void MainWindow::nxmRequestFailed(QString gameName, int modID, int, QVariant, int, QNetworkReply::NetworkError error, const QString &errorString)
{
if (error == QNetworkReply::ContentAccessDenied || error == QNetworkReply::ContentNotFoundError) {
- qDebug(qUtf8Printable(tr("Mod ID %1 no longer seems to be available on Nexus.").arg(modID)));
+ log::debug("{}", tr("Mod ID %1 no longer seems to be available on Nexus.").arg(modID));
} else {
MessageDialog::showMessage(tr("Request to Nexus failed: %1").arg(errorString), this);
}
@@ -6124,27 +6223,27 @@ void MainWindow::on_bsaList_itemChanged(QTreeWidgetItem*, int)
void MainWindow::on_actionNotifications_triggered()
{
updateProblemsButton();
- ProblemsDialog problems(m_PluginContainer.plugins<QObject>(), this);
- QSettings &settings = m_OrganizerCore.settings().directInterface();
- QString key = QString("geometry/%1").arg(problems.objectName());
- if (settings.contains(key)) {
- problems.restoreGeometry(settings.value(key).toByteArray());
- }
+ ProblemsDialog problems(m_PluginContainer.plugins<QObject>(), this);
problems.exec();
- settings.setValue(key, problems.saveGeometry());
+
updateProblemsButton();
}
void MainWindow::on_actionChange_Game_triggered()
{
- if (QMessageBox::question(this, tr("Are you sure?"),
- tr("This will restart MO, continue?"),
- QMessageBox::Yes | QMessageBox::Cancel)
- == QMessageBox::Yes) {
- InstanceManager::instance().clearCurrentInstance();
- qApp->exit(INT_MAX);
+ if (m_OrganizerCore.settings().interface().showChangeGameConfirmation()) {
+ const auto r = QMessageBox::question(
+ this, tr("Are you sure?"), tr("This will restart MO, continue?"),
+ QMessageBox::Yes | QMessageBox::Cancel);
+
+ if (r != QMessageBox::Yes) {
+ return;
+ }
}
+
+ InstanceManager::instance().clearCurrentInstance();
+ ExitModOrganizer(Exit::Restart);
}
void MainWindow::setCategoryListVisible(bool visible)
@@ -6163,35 +6262,68 @@ void MainWindow::on_displayCategoriesBtn_toggled(bool checked)
setCategoryListVisible(checked);
}
-void MainWindow::editCategories()
+void MainWindow::deselectFilters()
{
- CategoriesDialog dialog(this);
- QSettings &settings = m_OrganizerCore.settings().directInterface();
- QString key = QString("geometry/%1").arg(dialog.objectName());
- if (settings.contains(key)) {
- dialog.restoreGeometry(settings.value(key).toByteArray());
- }
- if (dialog.exec() == QDialog::Accepted) {
- dialog.commitChanges();
- }
- settings.setValue(key, dialog.saveGeometry());
-
+ m_Filters->clearSelection();
}
-void MainWindow::deselectFilters()
+void MainWindow::refreshFilters()
{
- ui->categoriesList->clearSelection();
+ QItemSelection currentSelection = ui->modList->selectionModel()->selection();
+
+ QVariant currentIndexName = ui->modList->currentIndex().data();
+ ui->modList->setCurrentIndex(QModelIndex());
+
+ m_Filters->refresh();
+
+ ui->modList->selectionModel()->select(currentSelection, QItemSelectionModel::Select);
+
+ QModelIndexList matchList;
+ if (currentIndexName.isValid()) {
+ matchList = ui->modList->model()->match(
+ ui->modList->model()->index(0, 0),
+ Qt::DisplayRole,
+ currentIndexName);
+ }
+
+ if (matchList.size() > 0) {
+ ui->modList->setCurrentIndex(matchList.at(0));
+ }
}
-void MainWindow::on_categoriesList_customContextMenuRequested(const QPoint &pos)
+void MainWindow::onFiltersCriteria(const std::vector<ModListSortProxy::Criteria>& criteria)
{
- QMenu menu;
- menu.addAction(tr("Edit Categories..."), this, SLOT(editCategories()));
- menu.addAction(tr("Deselect filter"), this, SLOT(deselectFilters()));
+ m_ModListSortProxy->setCriteria(criteria);
- menu.exec(ui->categoriesList->viewport()->mapToGlobal(pos));
+ QString label = "?";
+
+ if (criteria.empty()) {
+ label = "";
+ } else if (criteria.size() == 1) {
+ const auto& c = criteria[0];
+
+ if (c.type == ModListSortProxy::TypeContent) {
+ label = ModInfo::getContentTypeName(c.id);
+ } else {
+ label = m_CategoryFactory.getCategoryNameByID(c.id);
+ }
+
+ if (label.isEmpty()) {
+ log::error("category {}:{} not found", c.type, c.id);
+ }
+ } else {
+ label = tr("<Multiple>");
+ }
+
+ ui->currentCategoryLabel->setText(label);
+ ui->modList->reset();
}
+void MainWindow::onFiltersOptions(
+ ModListSortProxy::FilterMode mode, ModListSortProxy::SeparatorsMode sep)
+{
+ m_ModListSortProxy->setOptions(mode, sep);
+}
void MainWindow::updateESPLock(bool locked)
{
@@ -6227,9 +6359,7 @@ void MainWindow::removeFromToolbar()
auto itor = list.find(title);
if (itor == list.end()) {
- qWarning().nospace()
- << "removeFromToolbar(): executable '" << title << "' not found";
-
+ log::warn("removeFromToolbar(): executable '{}' not found", title);
return;
}
@@ -6356,259 +6486,35 @@ void MainWindow::on_groupCombo_currentIndexChanged(int index)
modFilterActive(m_ModListSortProxy->isFilterActive());
}
-const Executable &MainWindow::getSelectedExecutable() const
+Executable* MainWindow::getSelectedExecutable()
{
- const QString name = ui->executablesListBox->itemText(ui->executablesListBox->currentIndex());
- return m_OrganizerCore.executablesList()->get(name);
-}
-
-Executable &MainWindow::getSelectedExecutable()
-{
- const QString name = ui->executablesListBox->itemText(ui->executablesListBox->currentIndex());
- return m_OrganizerCore.executablesList()->get(name);
-}
+ const QString name = ui->executablesListBox->itemText(
+ ui->executablesListBox->currentIndex());
-void MainWindow::on_showHiddenBox_toggled(bool checked)
-{
- m_OrganizerCore.downloadManager()->setShowHidden(checked);
-}
-
-
-void MainWindow::createStdoutPipe(HANDLE *stdOutRead, HANDLE *stdOutWrite)
-{
- SECURITY_ATTRIBUTES secAttributes;
- secAttributes.nLength = sizeof(SECURITY_ATTRIBUTES);
- secAttributes.bInheritHandle = TRUE;
- secAttributes.lpSecurityDescriptor = nullptr;
-
- if (!::CreatePipe(stdOutRead, stdOutWrite, &secAttributes, 0)) {
- qCritical("failed to create stdout reroute");
- }
-
- if (!::SetHandleInformation(*stdOutRead, HANDLE_FLAG_INHERIT, 0)) {
- qCritical("failed to correctly set up the stdout reroute");
- *stdOutWrite = *stdOutRead = INVALID_HANDLE_VALUE;
+ try
+ {
+ return &m_OrganizerCore.executablesList()->get(name);
}
-}
-
-std::string MainWindow::readFromPipe(HANDLE stdOutRead)
-{
- static const int chunkSize = 128;
- std::string result;
-
- char buffer[chunkSize + 1];
- buffer[chunkSize] = '\0';
-
- DWORD read = 1;
- while (read > 0) {
- if (!::ReadFile(stdOutRead, buffer, chunkSize, &read, nullptr)) {
- break;
- }
- if (read > 0) {
- result.append(buffer, read);
- if (read < chunkSize) {
- break;
- }
- }
+ catch(std::runtime_error&)
+ {
+ return nullptr;
}
- return result;
}
-void MainWindow::processLOOTOut(const std::string &lootOut, std::string &errorMessages, QProgressDialog &dialog)
+void MainWindow::on_showHiddenBox_toggled(bool checked)
{
- std::vector<std::string> lines;
- boost::split(lines, lootOut, boost::is_any_of("\r\n"));
-
- std::regex exRequires("\"([^\"]*)\" requires \"([^\"]*)\", but it is missing\\.");
- std::regex exIncompatible("\"([^\"]*)\" is incompatible with \"([^\"]*)\", but both are present\\.");
-
- for (const std::string &line : lines) {
- if (line.length() > 0) {
- size_t progidx = line.find("[progress]");
- size_t erroridx = line.find("[error]");
- if (progidx != std::string::npos) {
- dialog.setLabelText(line.substr(progidx + 11).c_str());
- } else if (erroridx != std::string::npos) {
- qWarning("%s", line.c_str());
- errorMessages.append(boost::algorithm::trim_copy(line.substr(erroridx + 8)) + "\n");
- } else {
- std::smatch match;
- if (std::regex_match(line, match, exRequires)) {
- std::string modName(match[1].first, match[1].second);
- std::string dependency(match[2].first, match[2].second);
- m_OrganizerCore.pluginList()->addInformation(modName.c_str(), tr("depends on missing \"%1\"").arg(dependency.c_str()));
- } else if (std::regex_match(line, match, exIncompatible)) {
- std::string modName(match[1].first, match[1].second);
- std::string dependency(match[2].first, match[2].second);
- m_OrganizerCore.pluginList()->addInformation(modName.c_str(), tr("incompatible with \"%1\"").arg(dependency.c_str()));
- } else {
- qDebug("[loot] %s", line.c_str());
- }
- }
- }
- }
+ m_OrganizerCore.downloadManager()->setShowHidden(checked);
}
void MainWindow::on_bossButton_clicked()
{
- std::string reportURL;
- std::string errorMessages;
-
- //m_OrganizerCore.currentProfile()->writeModlistNow();
m_OrganizerCore.savePluginList();
- //Create a backup of the load orders w/ LOOT in name
- //to make sure that any sorting is easily undo-able.
- //Need to figure out how I want to do that.
-
- bool success = false;
-
- try {
- setEnabled(false);
- ON_BLOCK_EXIT([&] () { setEnabled(true); });
- QProgressDialog dialog(this);
- dialog.setLabelText(tr("Please wait while LOOT is running"));
- dialog.setMaximum(0);
- dialog.show();
-
- QString outPath = QDir::temp().absoluteFilePath("lootreport.json");
-
- QStringList parameters;
- parameters << "--game" << m_OrganizerCore.managedGame()->gameShortName()
- << "--gamePath" << QString("\"%1\"").arg(m_OrganizerCore.managedGame()->gameDirectory().absolutePath())
- << "--pluginListPath" << QString("\"%1/loadorder.txt\"").arg(m_OrganizerCore.profilePath())
- << "--out" << QString("\"%1\"").arg(outPath);
-
- if (m_DidUpdateMasterList) {
- parameters << "--skipUpdateMasterlist";
- }
- HANDLE stdOutWrite = INVALID_HANDLE_VALUE;
- HANDLE stdOutRead = INVALID_HANDLE_VALUE;
- createStdoutPipe(&stdOutRead, &stdOutWrite);
- try {
- m_OrganizerCore.prepareVFS();
- } catch (const UsvfsConnectorException &e) {
- qDebug(e.what());
- return;
- } catch (const std::exception &e) {
- QMessageBox::warning(qApp->activeWindow(), tr("Error"), e.what());
- return;
- }
-
- HANDLE loot = startBinary(QFileInfo(qApp->applicationDirPath() + "/loot/lootcli.exe"),
- parameters.join(" "),
- qApp->applicationDirPath() + "/loot",
- true,
- stdOutWrite);
-
- // we don't use the write end
- ::CloseHandle(stdOutWrite);
-
- m_OrganizerCore.pluginList()->clearAdditionalInformation();
-
- DWORD retLen;
- JOBOBJECT_BASIC_PROCESS_ID_LIST info;
- HANDLE processHandle = loot;
- if (loot != INVALID_HANDLE_VALUE) {
- bool isJobHandle = true;
- ULONG lastProcessID;
- DWORD res = ::MsgWaitForMultipleObjects(1, &loot, false, 100, QS_KEY | QS_MOUSE);
- while ((res != WAIT_FAILED) && (res != WAIT_OBJECT_0)) {
- if (isJobHandle) {
- if (::QueryInformationJobObject(loot, JobObjectBasicProcessIdList, &info, sizeof(info), &retLen) > 0) {
- if (info.NumberOfProcessIdsInList == 0) {
- qDebug("no more processes in job");
- break;
- } else {
- if (lastProcessID != info.ProcessIdList[0]) {
- lastProcessID = info.ProcessIdList[0];
- if (processHandle != loot) {
- ::CloseHandle(processHandle);
- }
- processHandle = ::OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, lastProcessID);
- }
- }
- } else {
- // the info-object I passed only provides space for 1 process id. but since this code only cares about whether there
- // is more than one that's good enough. ERROR_MORE_DATA simply signals there are at least two processes running.
- // any other error probably means the handle is a regular process handle, probably caused by running MO in a job without
- // the right to break out.
- if (::GetLastError() != ERROR_MORE_DATA) {
- isJobHandle = false;
- }
- }
- }
-
- if (dialog.wasCanceled()) {
- if (isJobHandle) {
- ::TerminateJobObject(loot, 1);
- } else {
- ::TerminateProcess(loot, 1);
- }
- }
-
- // keep processing events so the app doesn't appear dead
- QCoreApplication::processEvents();
- std::string lootOut = readFromPipe(stdOutRead);
- processLOOTOut(lootOut, errorMessages, dialog);
-
- res = ::MsgWaitForMultipleObjects(1, &loot, false, 100, QS_KEY | QS_MOUSE);
- }
-
- std::string remainder = readFromPipe(stdOutRead).c_str();
- if (remainder.length() > 0) {
- processLOOTOut(remainder, errorMessages, dialog);
- }
- DWORD exitCode = 0UL;
- ::GetExitCodeProcess(processHandle, &exitCode);
- ::CloseHandle(processHandle);
- if (exitCode != 0UL) {
- reportError(tr("loot failed. Exit code was: %1").arg(exitCode));
- return;
- } else {
- success = true;
- QFile outFile(outPath);
- outFile.open(QIODevice::ReadOnly);
- QJsonDocument doc = QJsonDocument::fromJson(outFile.readAll());
- QJsonArray array = doc.array();
- for (auto iter = array.begin(); iter != array.end(); ++iter) {
- QJsonObject pluginObj = (*iter).toObject();
- QJsonArray pluginMessages = pluginObj["messages"].toArray();
- for (auto msgIter = pluginMessages.begin(); msgIter != pluginMessages.end(); ++msgIter) {
- QJsonObject msg = (*msgIter).toObject();
- m_OrganizerCore.pluginList()->addInformation(pluginObj["name"].toString(),
- QString("%1: %2").arg(msg["type"].toString(), msg["message"].toString()));
- }
- if (pluginObj["dirty"].toString() == "yes")
- m_OrganizerCore.pluginList()->addInformation(pluginObj["name"].toString(), "dirty");
- }
-
- }
- } else {
- reportError(tr("failed to start loot"));
- }
- } catch (const std::exception &e) {
- reportError(tr("failed to run loot: %1").arg(e.what()));
- }
-
- if (errorMessages.length() > 0) {
- QMessageBox *warn = new QMessageBox(QMessageBox::Warning, tr("Errors occurred"), errorMessages.c_str(), QMessageBox::Ok, this);
- warn->setModal(false);
- warn->show();
- }
+ setEnabled(false);
+ ON_BLOCK_EXIT([&] () { setEnabled(true); });
- if (success) {
+ if (runLoot(this, m_OrganizerCore, m_DidUpdateMasterList)) {
m_DidUpdateMasterList = true;
- if (reportURL.length() > 0) {
- m_IntegratedBrowser.setWindowTitle("LOOT Report");
- QString report(reportURL.c_str());
- QStringList temp = report.split("?");
- QUrl url = QUrl::fromLocalFile(temp.at(0));
- if (temp.size() > 1) {
- url.setQuery(temp.at(1).toUtf8());
- }
- m_IntegratedBrowser.openUrl(url);
- }
m_OrganizerCore.refreshESPList(false);
m_OrganizerCore.savePluginList();
}
@@ -6683,8 +6589,13 @@ void MainWindow::on_restoreButton_clicked()
if (!shellCopy(pluginName + "." + choice, pluginName, true, this) ||
!shellCopy(loadOrderName + "." + choice, loadOrderName, true, this) ||
!shellCopy(lockedName + "." + choice, lockedName, true, this)) {
- QMessageBox::critical(this, tr("Restore failed"),
- tr("Failed to restore the backup. Errorcode: %1").arg(windowsErrorString(::GetLastError())));
+
+ const auto e = GetLastError();
+
+ QMessageBox::critical(
+ this, tr("Restore failed"),
+ tr("Failed to restore the backup. Errorcode: %1")
+ .arg(QString::fromStdWString(formatSystemMessage(e))));
}
m_OrganizerCore.refreshESPList(true);
}
@@ -6705,39 +6616,16 @@ void MainWindow::on_restoreModsButton_clicked()
QString choice = queryRestore(modlistName);
if (!choice.isEmpty()) {
if (!shellCopy(modlistName + "." + choice, modlistName, true, this)) {
- QMessageBox::critical(this, tr("Restore failed"),
- tr("Failed to restore the backup. Errorcode: %1").arg(windowsErrorString(::GetLastError())));
+ const auto e = GetLastError();
+ QMessageBox::critical(
+ this, tr("Restore failed"),
+ tr("Failed to restore the backup. Errorcode: %1")
+ .arg(formatSystemMessage(e)));
}
m_OrganizerCore.refreshModList(false);
}
}
-void MainWindow::on_actionCopy_Log_to_Clipboard_triggered()
-{
- QStringList lines;
- QAbstractItemModel *model = ui->logList->model();
- for (int i = 0; i < model->rowCount(); ++i) {
- lines.append(QString("%1 [%2] %3").arg(model->index(i, 0).data().toString())
- .arg(model->index(i, 1).data(Qt::UserRole).toString())
- .arg(model->index(i, 1).data().toString()));
- }
- QApplication::clipboard()->setText(lines.join("\n"));
-}
-
-void MainWindow::on_categoriesAndBtn_toggled(bool checked)
-{
- if (checked) {
- m_ModListSortProxy->setFilterMode(ModListSortProxy::FILTER_AND);
- }
-}
-
-void MainWindow::on_categoriesOrBtn_toggled(bool checked)
-{
- if (checked) {
- m_ModListSortProxy->setFilterMode(ModListSortProxy::FILTER_OR);
- }
-}
-
void MainWindow::on_managedArchiveLabel_linkHovered(const QString&)
{
QToolTip::showText(QCursor::pos(),
@@ -6794,7 +6682,7 @@ void MainWindow::dropLocalFile(const QUrl &url, const QString &outputDir, bool m
{
QFileInfo file(url.toLocalFile());
if (!file.exists()) {
- qWarning("invalid source file: %s", qUtf8Printable(file.absoluteFilePath()));
+ log::warn("invalid source file: {}", file.absoluteFilePath());
return;
}
QString target = outputDir + "/" + file.fileName();
@@ -6827,32 +6715,8 @@ void MainWindow::dropLocalFile(const QUrl &url, const QString &outputDir, bool m
success = shellCopy(file.absoluteFilePath(), target, true, this);
}
if (!success) {
- qCritical("file operation failed: %s", qUtf8Printable(windowsErrorString(::GetLastError())));
- }
-}
-
-bool MainWindow::registerWidgetState(const QString &name, QHeaderView *view, const char *oldSettingName) {
- // register the view so it's geometry gets saved at exit
- m_PersistedGeometry.push_back(std::make_pair(name, view));
-
- // also, restore the geometry if it was saved before
- QSettings &settings = m_OrganizerCore.settings().directInterface();
-
- QString key = QString("geometry/%1").arg(name);
- QByteArray data;
-
- if ((oldSettingName != nullptr) && settings.contains(oldSettingName)) {
- data = settings.value(oldSettingName).toByteArray();
- settings.remove(oldSettingName);
- } else if (settings.contains(key)) {
- data = settings.value(key).toByteArray();
- }
-
- if (!data.isEmpty()) {
- view->restoreState(data);
- return true;
- } else {
- return false;
+ const auto e = GetLastError();
+ log::error("file operation failed: {}", formatSystemMessage(e));
}
}
@@ -6879,18 +6743,13 @@ void MainWindow::keyReleaseEvent(QKeyEvent *event)
// if the menubar is hidden, pressing Alt will make it visible
if (event->key() == Qt::Key_Alt) {
if (!ui->menuBar->isVisible()) {
- showMenuBar(true);
+ ui->menuBar->show();
}
}
QMainWindow::keyReleaseEvent(event);
}
-void MainWindow::on_clickBlankButton_clicked()
-{
- deselectFilters();
-}
-
void MainWindow::on_clearFiltersButton_clicked()
{
ui->modFilterEdit->clear();
@@ -6946,12 +6805,9 @@ void MainWindow::sendSelectedModsToSeparator_clicked()
}
ListDialog dialog(this);
- QSettings &settings = m_OrganizerCore.settings().directInterface();
- QString key = QString("geometry/%1").arg(dialog.objectName());
-
dialog.setWindowTitle("Select a separator...");
dialog.setChoices(separators);
- dialog.restoreGeometry(settings.value(key).toByteArray());
+
if (dialog.exec() == QDialog::Accepted) {
QString result = dialog.getChoice();
if (!result.isEmpty()) {
@@ -6985,7 +6841,6 @@ void MainWindow::sendSelectedModsToSeparator_clicked()
}
}
}
- settings.setValue(key, dialog.saveGeometry());
}
void MainWindow::on_showArchiveDataCheckBox_toggled(const bool checked)
diff --git a/src/mainwindow.h b/src/mainwindow.h
index eee269cf..8f000983 100644
--- a/src/mainwindow.h
+++ b/src/mainwindow.h
@@ -30,18 +30,20 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#include "modlistsortproxy.h"
#include "savegameinfo.h"
#include "tutorialcontrol.h"
+#include "plugincontainer.h" //class PluginContainer;
+#include "iplugingame.h" //namespace MOBase { class IPluginGame; }
+#include <log.h>
//Note the commented headers here can be replaced with forward references,
//when I get round to cleaning up main.cpp
class Executable;
class CategoryFactory;
-class LockedDialogBase;
class OrganizerCore;
-class StatusBar;
-#include "plugincontainer.h" //class PluginContainer;
+class FilterList;
+
class PluginListSortProxy;
namespace BSA { class Archive; }
-#include "iplugingame.h" //namespace MOBase { class IPluginGame; }
+
namespace MOBase { class IPluginModPage; }
namespace MOBase { class IPluginTool; }
namespace MOBase { class ISaveGame; }
@@ -80,7 +82,6 @@ class QProgressDialog;
class QTranslator;
class QTreeWidgetItem;
class QUrl;
-class QSettings;
class QWidget;
#ifndef Q_MOC_RUN
@@ -100,6 +101,7 @@ namespace Ui {
class MainWindow;
}
+class Settings;
class MainWindow : public QMainWindow, public IUserInterface
@@ -109,18 +111,14 @@ class MainWindow : public QMainWindow, public IUserInterface
friend class OrganizerProxy;
public:
-
- explicit MainWindow(QSettings &initSettings,
+ explicit MainWindow(Settings &settings,
OrganizerCore &organizerCore, PluginContainer &pluginContainer,
QWidget *parent = 0);
~MainWindow();
- void storeSettings(QSettings &settings) override;
- void readSettings();
void processUpdates();
- virtual ILockedWaitingForProcess* lock() override;
- virtual void unlock() override;
+ QWidget* qtWidget() override;
bool addProfile();
void updateBSAList(const QStringList &defaultArchives, const QStringList &activeArchives);
@@ -139,10 +137,6 @@ public:
void addPrimaryCategoryCandidates(QMenu *primaryCategoryMenu, ModInfo::Ptr info);
- void createStdoutPipe(HANDLE *stdOutRead, HANDLE *stdOutWrite);
- std::string readFromPipe(HANDLE stdOutRead);
- void processLOOTOut(const std::string &lootOut, std::string &errorMessages, QProgressDialog &dialog);
-
void updateModInDirectoryStructure(unsigned int index, ModInfo::Ptr modInfo);
QString getOriginDisplayName(int originID);
@@ -154,7 +148,8 @@ public:
void displayModInformation(
ModInfo::Ptr modInfo, unsigned int modIndex, ModInfoTabIDs tabID) override;
- bool confirmExit();
+ bool canExit();
+ void onBeforeClose();
virtual bool closeWindow();
virtual void setWindowEnabled(bool enabled);
@@ -194,6 +189,7 @@ signals:
protected:
virtual void showEvent(QShowEvent *event);
+ virtual void paintEvent(QPaintEvent* event);
virtual void closeEvent(QCloseEvent *event);
virtual bool eventFilter(QObject *obj, QEvent *event);
virtual void resizeEvent(QResizeEvent *event);
@@ -204,9 +200,6 @@ protected:
private slots:
void on_actionChange_Game_triggered();
-private slots:
- void on_clickBlankButton_clicked();
-
private:
void cleanup();
@@ -219,13 +212,13 @@ private:
void updatePinnedExecutables();
void setToolbarSize(const QSize& s);
void setToolbarButtonStyle(Qt::ToolButtonStyle s);
- void toolbarMenu_aboutToShow();
+
+ void updateToolbarMenu();
+ void updateViewMenu();
QMenu* createPopupMenu() override;
void activateSelectedProfile();
- void setExecutableIndex(int index);
-
void startSteam();
void updateTo(QTreeWidgetItem *subTree, const std::wstring &directorySoFar, const MOShared::DirectoryEntry &directoryEntry, bool conflictsOnly, QIcon *fileIcon, QIcon *folderIcon);
@@ -235,14 +228,12 @@ private:
QList<MOBase::IOrganizer::FileInfo> findFileInfos(const QString &path, const std::function<bool (const MOBase::IOrganizer::FileInfo &)> &filter) const;
- bool modifyExecutablesDialog();
+ bool modifyExecutablesDialog(int selection);
void displayModInformation(int row, ModInfoTabIDs tab=ModInfoTabIDs::None);
void testExtractBSA(int modIndex);
void writeDataToFile(QFile &file, const QString &directory, const MOShared::DirectoryEntry &directoryEntry);
- void refreshFilters();
-
/**
* Sets category selections from menu; for multiple mods, this will only apply
* the changes made in the menu (which is the delta between the current menu selection and the reference mod)
@@ -272,10 +263,6 @@ private:
size_t checkForProblems();
- QTreeWidgetItem *addFilterItem(QTreeWidgetItem *root, const QString &name, int categoryID, ModListSortProxy::FilterType type);
- void addContentFilters();
- void addCategoryFilters(QTreeWidgetItem *root, const std::set<int> &categoriesUsed, int targetID);
-
void setCategoryListVisible(bool visible);
void displaySaveGameInfo(QListWidgetItem *newItem);
@@ -288,7 +275,6 @@ private:
static void setupNetworkProxy(bool activate);
void activateProxy(bool activate);
- void setBrowserGeometry(const QByteArray &geometry);
bool createBackup(const QString &filePath, const QDateTime &time);
QString queryRestore(const QString &filePath);
@@ -310,12 +296,11 @@ private:
void dropLocalFile(const QUrl &url, const QString &outputDir, bool move);
- bool registerWidgetState(const QString &name, QHeaderView *view, const char *oldSettingName = nullptr);
-
void sendSelectedModsToPriority(int newPriority);
void sendSelectedPluginsToPriority(int newPriority);
void toggleMO2EndorseState();
+ void toggleUpdateAction();
private:
@@ -328,12 +313,7 @@ private:
Ui::MainWindow *ui;
bool m_WasVisible;
-
- // this has to be remembered because by the time storeSettings() is called,
- // the window is closed and the all bars are hidden
- bool m_menuBarVisible, m_statusBarVisible;
-
- std::unique_ptr<StatusBar> m_statusBar;
+ bool m_FirstPaint;
// last separator on the toolbar, used to add spacer for right-alignment and
// as an insert point for executables
@@ -341,14 +321,14 @@ private:
MOBase::TutorialControl m_Tutorial;
+ std::unique_ptr<FilterList> m_Filters;
+
int m_OldProfileIndex;
std::vector<QString> m_ModNameList; // the mod-list to go with the directory structure
- bool m_Refreshing;
QStringList m_DefaultArchives;
- QAbstractItemModel *m_ModListGroupingProxy;
ModListSortProxy *m_ModListSortProxy;
PluginListSortProxy *m_PluginListSortProxy;
@@ -364,8 +344,6 @@ private:
CategoryFactory &m_CategoryFactory;
- bool m_LoginAttempted;
-
QTimer m_CheckBSATimer;
QTimer m_SaveMetaTimer;
QTimer m_UpdateProblemsTimer;
@@ -392,15 +370,8 @@ private:
bool m_DidUpdateMasterList;
- LockedDialogBase *m_LockDialog { nullptr };
- uint64_t m_LockCount { 0 };
-
- bool m_closing{ false };
-
bool m_showArchiveData{ true };
- std::vector<std::pair<QString, QHeaderView*>> m_PersistedGeometry;
-
MOBase::DelayedFileWriter m_ArchiveListWriter;
QAction* m_LinkToolbar;
@@ -411,8 +382,7 @@ private:
// when painting the count
QIcon m_originalNotificationIcon;
- Executable const &getSelectedExecutable() const;
- Executable &getSelectedExecutable();
+ Executable* getSelectedExecutable();
private slots:
@@ -469,11 +439,16 @@ private slots:
// data-tree context menu
void writeDataToFile();
void openDataFile();
+ void openDataFile(QTreeWidgetItem* item);
+ void runDataFileHooked();
+ void runDataFileHooked(QTreeWidgetItem* item);
void addAsExecutable();
void previewDataFile();
+ void previewDataFile(QTreeWidgetItem* item);
void hideFile();
void unhideFile();
void openDataOriginExplorer_clicked();
+ void openDataModInfo_clicked();
// pluginlist context menu
void enableSelectedPlugins_clicked();
@@ -504,14 +479,9 @@ private slots:
void doMoveOverwriteContentToMod(const QString &modAbsolutePath);
void clearOverwrite();
- void procError(QProcess::ProcessError error);
- void procFinished(int exitCode, QProcess::ExitStatus exitStatus);
-
// nexus related
void checkModsForUpdates();
- void validationFailed(const QString &message);
-
void linkClicked(const QString &url);
void updateAvailable();
@@ -545,8 +515,11 @@ private slots:
void onRequestsChanged(const APIStats& stats, const APIUserAccount& user);
- void editCategories();
void deselectFilters();
+ void refreshFilters();
+ void onFiltersCriteria(const std::vector<ModListSortProxy::Criteria>& filters);
+ void onFiltersOptions(
+ ModListSortProxy::FilterMode mode, ModListSortProxy::SeparatorsMode sep);
void displayModInformation(const QString &modName, ModInfoTabIDs tabID);
@@ -573,6 +546,7 @@ private slots:
void openLogsFolder();
void openInstallFolder();
void openPluginsFolder();
+ void openStylesheetsFolder();
void openDownloadsFolder();
void openModsFolder();
void openProfileFolder();
@@ -600,10 +574,7 @@ private slots:
void expandModList(const QModelIndex &index);
- /**
- * @brief resize columns in mod list and plugin list to content
- */
- void resizeLists(bool modListCustom, bool pluginListCustom);
+ void resizeLists(bool pluginListCustom);
/**
* @brief allow columns in mod list and plugin list to be resized
@@ -621,6 +592,7 @@ private slots:
void refreshSavesIfOpen();
void expandDataTreeItem(QTreeWidgetItem *item);
+ void activateDataTreeItem(QTreeWidgetItem *item, int column);
void about();
void delayedRemove();
@@ -656,13 +628,13 @@ private slots: // ui slots
void on_actionToolBarIconsOnly_triggered();
void on_actionToolBarTextOnly_triggered();
void on_actionToolBarIconsAndText_triggered();
+ void on_actionViewLog_triggered();
void on_centralWidget_customContextMenuRequested(const QPoint &pos);
void on_bsaList_customContextMenuRequested(const QPoint &pos);
void on_clearFiltersButton_clicked();
void on_btnRefreshData_clicked();
void on_btnRefreshDownloads_clicked();
- void on_categoriesList_customContextMenuRequested(const QPoint &pos);
void on_conflictsCheckBox_toggled(bool checked);
void on_showArchiveDataCheckBox_toggled(bool checked);
void on_dataTree_customContextMenuRequested(const QPoint &pos);
@@ -679,7 +651,6 @@ private slots: // ui slots
void on_espList_customContextMenuRequested(const QPoint &pos);
void on_displayCategoriesBtn_toggled(bool checked);
void on_groupCombo_currentIndexChanged(int index);
- void on_categoriesList_itemSelectionChanged();
void on_linkButton_pressed();
void on_showHiddenBox_toggled(bool checked);
void on_bsaList_itemChanged(QTreeWidgetItem *item, int column);
@@ -689,15 +660,11 @@ private slots: // ui slots
void on_restoreButton_clicked();
void on_restoreModsButton_clicked();
void on_saveModsButton_clicked();
- void on_actionCopy_Log_to_Clipboard_triggered();
- void on_categoriesAndBtn_toggled(bool checked);
- void on_categoriesOrBtn_toggled(bool checked);
void on_managedArchiveLabel_linkHovered(const QString &link);
- void showMenuBar(bool b);
- void showStatusBar(bool b);
+ void storeSettings();
+ void readSettings();
+ void setupModList();
};
-
-
#endif // MAINWINDOW_H
diff --git a/src/mainwindow.ui b/src/mainwindow.ui
index 70d1cf39..a7e7b937 100644
--- a/src/mainwindow.ui
+++ b/src/mainwindow.ui
@@ -33,526 +33,700 @@
<property name="contextMenuPolicy">
<enum>Qt::CustomContextMenu</enum>
</property>
- <layout class="QHBoxLayout" name="horizontalLayout_8">
- <property name="leftMargin">
- <number>6</number>
- </property>
- <property name="topMargin">
- <number>6</number>
- </property>
- <property name="rightMargin">
- <number>6</number>
- </property>
- <property name="bottomMargin">
- <number>0</number>
- </property>
+ <layout class="QVBoxLayout" name="verticalLayout_8">
<item>
- <widget class="QSplitter" name="topLevelSplitter">
+ <widget class="QSplitter" name="categoriesSplitter">
<property name="orientation">
- <enum>Qt::Vertical</enum>
+ <enum>Qt::Horizontal</enum>
</property>
- <widget class="QWidget" name="horizontalLayoutWidget_2">
- <layout class="QHBoxLayout" name="horizontalLayout_10" stretch="0,2">
+ <property name="childrenCollapsible">
+ <bool>false</bool>
+ </property>
+ <widget class="QGroupBox" name="categoriesGroup">
+ <property name="maximumSize">
+ <size>
+ <width>500</width>
+ <height>16777215</height>
+ </size>
+ </property>
+ <property name="title">
+ <string>Filters</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_10" stretch="6,0,0">
+ <property name="spacing">
+ <number>0</number>
+ </property>
+ <property name="leftMargin">
+ <number>3</number>
+ </property>
+ <property name="topMargin">
+ <number>7</number>
+ </property>
+ <property name="rightMargin">
+ <number>3</number>
+ </property>
+ <property name="bottomMargin">
+ <number>1</number>
+ </property>
<item>
- <layout class="QVBoxLayout" name="verticalLayout_8">
- <item>
- <widget class="QGroupBox" name="categoriesGroup">
- <property name="title">
- <string>Categories</string>
- </property>
- <layout class="QVBoxLayout" name="verticalLayout_10" stretch="6,0,0">
- <property name="spacing">
- <number>0</number>
- </property>
- <property name="leftMargin">
- <number>3</number>
- </property>
- <property name="topMargin">
- <number>7</number>
- </property>
- <property name="rightMargin">
- <number>3</number>
+ <widget class="QTreeWidget" name="filters">
+ <property name="minimumSize">
+ <size>
+ <width>120</width>
+ <height>0</height>
+ </size>
+ </property>
+ <property name="contextMenuPolicy">
+ <enum>Qt::NoContextMenu</enum>
+ </property>
+ <property name="alternatingRowColors">
+ <bool>true</bool>
+ </property>
+ <property name="selectionMode">
+ <enum>QAbstractItemView::NoSelection</enum>
+ </property>
+ <property name="indentation">
+ <number>0</number>
+ </property>
+ <property name="rootIsDecorated">
+ <bool>false</bool>
+ </property>
+ <property name="uniformRowHeights">
+ <bool>true</bool>
+ </property>
+ <property name="allColumnsShowFocus">
+ <bool>true</bool>
+ </property>
+ <property name="headerHidden">
+ <bool>true</bool>
+ </property>
+ <attribute name="headerVisible">
+ <bool>false</bool>
+ </attribute>
+ <attribute name="headerCascadingSectionResizes">
+ <bool>false</bool>
+ </attribute>
+ <column>
+ <property name="text">
+ <string/>
+ </property>
+ </column>
+ <column>
+ <property name="text">
+ <string notr="true">Category</string>
+ </property>
+ </column>
+ </widget>
+ </item>
+ <item>
+ <widget class="QWidget" name="widget_2" native="true">
+ <layout class="QHBoxLayout" name="horizontalLayout_8">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>2</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QPushButton" name="filtersClear">
+ <property name="text">
+ <string>Clear</string>
</property>
- <property name="bottomMargin">
- <number>1</number>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="filtersEdit">
+ <property name="text">
+ <string>Edit...</string>
</property>
- <item>
- <widget class="QTreeWidget" name="categoriesList">
- <property name="minimumSize">
- <size>
- <width>120</width>
- <height>0</height>
- </size>
- </property>
- <property name="maximumSize">
- <size>
- <width>214</width>
- <height>16777215</height>
- </size>
- </property>
- <property name="contextMenuPolicy">
- <enum>Qt::CustomContextMenu</enum>
- </property>
- <property name="selectionMode">
- <enum>QAbstractItemView::ExtendedSelection</enum>
- </property>
- <property name="indentation">
- <number>0</number>
- </property>
- <property name="uniformRowHeights">
- <bool>true</bool>
- </property>
- <attribute name="headerVisible">
- <bool>false</bool>
- </attribute>
- <column>
- <property name="text">
- <string notr="true">1</string>
- </property>
- </column>
- </widget>
- </item>
- <item>
- <widget class="QPushButton" name="clickBlankButton">
- <property name="enabled">
- <bool>false</bool>
- </property>
- <property name="sizePolicy">
- <sizepolicy hsizetype="Minimum" vsizetype="Maximum">
- <horstretch>0</horstretch>
- <verstretch>0</verstretch>
- </sizepolicy>
- </property>
- <property name="minimumSize">
- <size>
- <width>0</width>
- <height>25</height>
- </size>
- </property>
- <property name="text">
- <string>Clear</string>
- </property>
- <property name="flat">
- <bool>true</bool>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QWidget" name="widget" native="true">
- <property name="sizePolicy">
- <sizepolicy hsizetype="Preferred" vsizetype="Preferred">
- <horstretch>0</horstretch>
- <verstretch>0</verstretch>
- </sizepolicy>
- </property>
- <layout class="QHBoxLayout" name="horizontalLayout_11">
- <item>
- <widget class="QRadioButton" name="categoriesAndBtn">
- <property name="toolTip">
- <string>If checked, only mods that match all selected categories are displayed.</string>
- </property>
- <property name="text">
- <string>And</string>
- </property>
- <property name="checked">
- <bool>true</bool>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QRadioButton" name="categoriesOrBtn">
- <property name="toolTip">
- <string>If checked, all mods that match at least one of the selected categories are displayed.</string>
- </property>
- <property name="text">
- <string>Or</string>
- </property>
- </widget>
- </item>
- </layout>
- </widget>
- </item>
- </layout>
- </widget>
- </item>
- </layout>
+ </widget>
+ </item>
+ </layout>
+ </widget>
</item>
<item>
- <widget class="QSplitter" name="splitter">
+ <widget class="QWidget" name="widget" native="true">
<property name="sizePolicy">
- <sizepolicy hsizetype="Expanding" vsizetype="Expanding">
+ <sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
- <property name="orientation">
- <enum>Qt::Horizontal</enum>
- </property>
- <widget class="QWidget" name="layoutWidget">
- <layout class="QVBoxLayout" name="verticalLayout">
- <property name="spacing">
- <number>2</number>
- </property>
- <item>
- <layout class="QHBoxLayout" name="horizontalLayout_6" stretch="0,1,0,0,0,0,0,0,0">
- <item>
- <widget class="QLabel" name="label_3">
- <property name="sizePolicy">
- <sizepolicy hsizetype="Fixed" vsizetype="Preferred">
- <horstretch>0</horstretch>
- <verstretch>0</verstretch>
- </sizepolicy>
- </property>
- <property name="text">
- <string>Profile</string>
- </property>
- <property name="buddy">
- <cstring>profileBox</cstring>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QComboBox" name="profileBox">
- <property name="toolTip">
- <string>Pick a module collection</string>
- </property>
- <property name="whatsThis">
- <string>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;
+ <layout class="QHBoxLayout" name="horizontalLayout_11">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>2</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QRadioButton" name="filtersAnd">
+ <property name="toolTip">
+ <string>Display mods that match all selected categories.</string>
+ </property>
+ <property name="text">
+ <string>And</string>
+ </property>
+ <property name="checked">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QRadioButton" name="filtersOr">
+ <property name="toolTip">
+ <string>Display mods that match at least one of the selected categories</string>
+ </property>
+ <property name="text">
+ <string>Or</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QComboBox" name="filtersSeparators">
+ <property name="toolTip">
+ <string>Filter: only show the separators that match the current filters
+Show: always show separators
+Hide: never show separators</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <widget class="QSplitter" name="splitter">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Expanding">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <widget class="QWidget" name="layoutWidget">
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <property name="spacing">
+ <number>2</number>
+ </property>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_6" stretch="0,1,0,0,0,0,0,0,0">
+ <item>
+ <widget class="QLabel" name="label_3">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>Profile</string>
+ </property>
+ <property name="buddy">
+ <cstring>profileBox</cstring>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QComboBox" name="profileBox">
+ <property name="toolTip">
+ <string>Pick a module collection</string>
+ </property>
+ <property name="whatsThis">
+ <string>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;
&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;
p, li { white-space: pre-wrap; }
&lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:'MS Shell Dlg 2'; font-size:8.25pt; font-weight:400; font-style:normal;&quot;&gt;
&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-size:8pt;&quot;&gt;Create profiles here. Each profile contains its own list of active mods and esps. This way you can quickly switch between setups for different playthroughs.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-size:8pt;&quot;&gt;Please note that right now your esp load order is not kept separate for different profiles.&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
- </property>
- </widget>
- </item>
- <item>
- <spacer name="horizontalSpacer">
- <property name="orientation">
- <enum>Qt::Horizontal</enum>
- </property>
- <property name="sizeHint" stdset="0">
- <size>
- <width>40</width>
- <height>20</height>
- </size>
- </property>
- </spacer>
- </item>
- <item>
- <widget class="QPushButton" name="listOptionsBtn">
- <property name="maximumSize">
- <size>
- <width>16777215</width>
- <height>16777215</height>
- </size>
- </property>
- <property name="toolTip">
- <string>Open list options...</string>
- </property>
- <property name="whatsThis">
- <string>Refresh list. This is usually not necessary unless you modified data outside the program.</string>
- </property>
- <property name="text">
- <string/>
- </property>
- <property name="icon">
- <iconset resource="resources.qrc">
- <normaloff>:/MO/gui/settings</normaloff>:/MO/gui/settings</iconset>
- </property>
- <property name="iconSize">
- <size>
- <width>16</width>
- <height>16</height>
- </size>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QPushButton" name="openFolderMenu">
- <property name="toolTip">
- <string>Show Open Folders menu...</string>
- </property>
- <property name="text">
- <string/>
- </property>
- <property name="icon">
- <iconset resource="resources.qrc">
- <normaloff>:/MO/gui/open_folder</normaloff>:/MO/gui/open_folder</iconset>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QPushButton" name="restoreModsButton">
- <property name="toolTip">
- <string>Restore Backup...</string>
- </property>
- <property name="text">
- <string notr="true"/>
- </property>
- <property name="icon">
- <iconset resource="resources.qrc">
- <normaloff>:/MO/gui/restore</normaloff>:/MO/gui/restore</iconset>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QPushButton" name="saveModsButton">
- <property name="toolTip">
- <string>Create Backup</string>
- </property>
- <property name="text">
- <string notr="true"/>
- </property>
- <property name="icon">
- <iconset resource="resources.qrc">
- <normaloff>:/MO/gui/backup</normaloff>:/MO/gui/backup</iconset>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QLabel" name="activeModslabel">
- <property name="text">
- <string>Active:</string>
- </property>
- </widget>
- </item>
- <item>
- <widget class="LCDNumber" name="activeModsCounter">
- <property name="minimumSize">
- <size>
- <width>0</width>
- <height>26</height>
- </size>
- </property>
- <property name="whatsThis">
- <string>This provides statistics about the mod list. The total number of active mod is normally displayed. Other statistics may be accessed with the tooltip of this counter.</string>
- </property>
- <property name="frameShadow">
- <enum>QFrame::Sunken</enum>
- </property>
- <property name="digitCount">
- <number>5</number>
- </property>
- <property name="segmentStyle">
- <enum>QLCDNumber::Flat</enum>
- </property>
- </widget>
- </item>
- </layout>
- </item>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>40</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QPushButton" name="listOptionsBtn">
+ <property name="maximumSize">
+ <size>
+ <width>16777215</width>
+ <height>16777215</height>
+ </size>
+ </property>
+ <property name="toolTip">
+ <string>Open list options...</string>
+ </property>
+ <property name="whatsThis">
+ <string>Refresh list. This is usually not necessary unless you modified data outside the program.</string>
+ </property>
+ <property name="text">
+ <string/>
+ </property>
+ <property name="icon">
+ <iconset resource="resources.qrc">
+ <normaloff>:/MO/gui/settings</normaloff>:/MO/gui/settings</iconset>
+ </property>
+ <property name="iconSize">
+ <size>
+ <width>16</width>
+ <height>16</height>
+ </size>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="openFolderMenu">
+ <property name="toolTip">
+ <string>Show Open Folders menu...</string>
+ </property>
+ <property name="text">
+ <string/>
+ </property>
+ <property name="icon">
+ <iconset resource="resources.qrc">
+ <normaloff>:/MO/gui/open_folder</normaloff>:/MO/gui/open_folder</iconset>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="restoreModsButton">
+ <property name="toolTip">
+ <string>Restore Backup...</string>
+ </property>
+ <property name="text">
+ <string notr="true"/>
+ </property>
+ <property name="icon">
+ <iconset resource="resources.qrc">
+ <normaloff>:/MO/gui/restore</normaloff>:/MO/gui/restore</iconset>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="saveModsButton">
+ <property name="toolTip">
+ <string>Create Backup</string>
+ </property>
+ <property name="text">
+ <string notr="true"/>
+ </property>
+ <property name="icon">
+ <iconset resource="resources.qrc">
+ <normaloff>:/MO/gui/backup</normaloff>:/MO/gui/backup</iconset>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="activeModslabel">
+ <property name="text">
+ <string>Active:</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="LCDNumber" name="activeModsCounter">
+ <property name="minimumSize">
+ <size>
+ <width>0</width>
+ <height>26</height>
+ </size>
+ </property>
+ <property name="whatsThis">
+ <string>This provides statistics about the mod list. The total number of active mod is normally displayed. Other statistics may be accessed with the tooltip of this counter.</string>
+ </property>
+ <property name="frameShadow">
+ <enum>QFrame::Sunken</enum>
+ </property>
+ <property name="digitCount">
+ <number>5</number>
+ </property>
+ <property name="segmentStyle">
+ <enum>QLCDNumber::Flat</enum>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <widget class="ModListView" name="modList">
+ <property name="minimumSize">
+ <size>
+ <width>330</width>
+ <height>400</height>
+ </size>
+ </property>
+ <property name="contextMenuPolicy">
+ <enum>Qt::CustomContextMenu</enum>
+ </property>
+ <property name="whatsThis">
+ <string>This is a list of installed mods. Use the checkboxes to activate/deactivate mods and drag &amp; drop mods to change their &quot;installation&quot; orders.</string>
+ </property>
+ <property name="styleSheet">
+ <string notr="true"/>
+ </property>
+ <property name="editTriggers">
+ <set>QAbstractItemView::EditKeyPressed|QAbstractItemView::SelectedClicked</set>
+ </property>
+ <property name="showDropIndicator" stdset="0">
+ <bool>true</bool>
+ </property>
+ <property name="dragEnabled">
+ <bool>true</bool>
+ </property>
+ <property name="dragDropMode">
+ <enum>QAbstractItemView::DragDrop</enum>
+ </property>
+ <property name="defaultDropAction">
+ <enum>Qt::MoveAction</enum>
+ </property>
+ <property name="alternatingRowColors">
+ <bool>true</bool>
+ </property>
+ <property name="selectionMode">
+ <enum>QAbstractItemView::ExtendedSelection</enum>
+ </property>
+ <property name="selectionBehavior">
+ <enum>QAbstractItemView::SelectRows</enum>
+ </property>
+ <property name="indentation">
+ <number>20</number>
+ </property>
+ <property name="uniformRowHeights">
+ <bool>true</bool>
+ </property>
+ <property name="itemsExpandable">
+ <bool>true</bool>
+ </property>
+ <property name="sortingEnabled">
+ <bool>true</bool>
+ </property>
+ <property name="expandsOnDoubleClick">
+ <bool>false</bool>
+ </property>
+ <attribute name="headerDefaultSectionSize">
+ <number>35</number>
+ </attribute>
+ <attribute name="headerShowSortIndicator" stdset="0">
+ <bool>true</bool>
+ </attribute>
+ <attribute name="headerStretchLastSection">
+ <bool>false</bool>
+ </attribute>
+ </widget>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_4" stretch="0,0,1,1,0,1,1">
+ <item>
+ <widget class="QPushButton" name="displayCategoriesBtn">
+ <property name="maximumSize">
+ <size>
+ <width>20</width>
+ <height>16777215</height>
+ </size>
+ </property>
+ <property name="text">
+ <string notr="true">x</string>
+ </property>
+ <property name="iconSize">
+ <size>
+ <width>20</width>
+ <height>20</height>
+ </size>
+ </property>
+ <property name="checkable">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="label_2">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Minimum" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>Filter</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="currentCategoryLabel"/>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer_5">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>40</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item alignment="Qt::AlignLeft">
+ <widget class="QPushButton" name="clearFiltersButton">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Maximum">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>0</width>
+ <height>22</height>
+ </size>
+ </property>
+ <property name="baseSize">
+ <size>
+ <width>95</width>
+ <height>0</height>
+ </size>
+ </property>
+ <property name="visible">
+ <bool>false</bool>
+ </property>
+ <property name="layoutDirection">
+ <enum>Qt::RightToLeft</enum>
+ </property>
+ <property name="styleSheet">
+ <string notr="true">border:1px solid #ff0000;</string>
+ </property>
+ <property name="text">
+ <string>Clear all Filters</string>
+ </property>
+ <property name="icon">
+ <iconset resource="resources.qrc">
+ <normaloff>:/MO/gui/edit_clear</normaloff>:/MO/gui/edit_clear</iconset>
+ </property>
+ <property name="iconSize">
+ <size>
+ <width>12</width>
+ <height>12</height>
+ </size>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QComboBox" name="groupCombo">
+ <property name="baseSize">
+ <size>
+ <width>220</width>
+ <height>0</height>
+ </size>
+ </property>
+ <property name="focusPolicy">
+ <enum>Qt::ClickFocus</enum>
+ </property>
+ <item>
+ <property name="text">
+ <string>No groups</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>Categories</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>Nexus IDs</string>
+ </property>
+ </item>
+ </widget>
+ </item>
+ <item>
+ <widget class="MOBase::LineEditClear" name="modFilterEdit">
+ <property name="baseSize">
+ <size>
+ <width>220</width>
+ <height>0</height>
+ </size>
+ </property>
+ <property name="placeholderText">
+ <string>Filter</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </widget>
+ <widget class="QWidget" name="layoutWidget_2">
+ <layout class="QVBoxLayout" name="verticalLayout_2">
+ <item>
+ <widget class="QFrame" name="startGroup">
+ <layout class="QHBoxLayout" name="horizontalLayout_5" stretch="1,0">
<item>
- <widget class="ModListView" name="modList">
+ <widget class="QComboBox" name="executablesListBox">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
<property name="minimumSize">
<size>
- <width>330</width>
- <height>400</height>
+ <width>0</width>
+ <height>40</height>
</size>
</property>
- <property name="contextMenuPolicy">
- <enum>Qt::CustomContextMenu</enum>
+ <property name="font">
+ <font>
+ <pointsize>9</pointsize>
+ <weight>75</weight>
+ <bold>true</bold>
+ </font>
</property>
<property name="toolTip">
- <string>List of available mods.</string>
+ <string>Pick a program to run.</string>
</property>
<property name="whatsThis">
- <string>This is a list of installed mods. Use the checkboxes to activate/deactivate mods and drag &amp; drop mods to change their &quot;installation&quot; orders.</string>
- </property>
- <property name="styleSheet">
- <string notr="true"/>
- </property>
- <property name="editTriggers">
- <set>QAbstractItemView::EditKeyPressed|QAbstractItemView::SelectedClicked</set>
- </property>
- <property name="showDropIndicator" stdset="0">
- <bool>true</bool>
- </property>
- <property name="dragEnabled">
- <bool>true</bool>
- </property>
- <property name="dragDropMode">
- <enum>QAbstractItemView::DragDrop</enum>
- </property>
- <property name="defaultDropAction">
- <enum>Qt::MoveAction</enum>
- </property>
- <property name="alternatingRowColors">
- <bool>true</bool>
- </property>
- <property name="selectionMode">
- <enum>QAbstractItemView::ExtendedSelection</enum>
- </property>
- <property name="selectionBehavior">
- <enum>QAbstractItemView::SelectRows</enum>
- </property>
- <property name="indentation">
- <number>20</number>
- </property>
- <property name="uniformRowHeights">
- <bool>true</bool>
- </property>
- <property name="itemsExpandable">
- <bool>true</bool>
+ <string>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;
+&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;
+p, li { white-space: pre-wrap; }
+&lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:'MS Shell Dlg 2'; font-size:8.25pt; font-weight:400; font-style:normal;&quot;&gt;
+&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-size:8pt;&quot;&gt;Choose the program to run. Once you start using ModOrganizer, you should always run your game and tools from here or through shortcuts created here, otherwise mods installed through MO will not be visible.&lt;/span&gt;&lt;/p&gt;
+&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-size:8pt;&quot;&gt;You can add new Tools to this list, but I can't promise tools I haven't tested will work.&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
- <property name="sortingEnabled">
- <bool>true</bool>
+ <property name="iconSize">
+ <size>
+ <width>32</width>
+ <height>32</height>
+ </size>
</property>
- <property name="expandsOnDoubleClick">
+ <property name="frame">
<bool>false</bool>
</property>
- <attribute name="headerDefaultSectionSize">
- <number>35</number>
- </attribute>
- <attribute name="headerShowSortIndicator" stdset="0">
- <bool>true</bool>
- </attribute>
- <attribute name="headerStretchLastSection">
- <bool>false</bool>
- </attribute>
</widget>
</item>
<item>
- <layout class="QHBoxLayout" name="horizontalLayout_4" stretch="0,0,1,1,0,1,1">
- <item>
- <widget class="QPushButton" name="displayCategoriesBtn">
- <property name="maximumSize">
- <size>
- <width>20</width>
- <height>16777215</height>
- </size>
- </property>
- <property name="text">
- <string notr="true">x</string>
- </property>
- <property name="iconSize">
- <size>
- <width>20</width>
- <height>20</height>
- </size>
- </property>
- <property name="checkable">
- <bool>true</bool>
- </property>
- </widget>
- </item>
+ <layout class="QVBoxLayout" name="verticalLayout_12" stretch="0,0">
<item>
- <widget class="QLabel" name="label_2">
+ <widget class="QPushButton" name="startButton">
<property name="sizePolicy">
- <sizepolicy hsizetype="Minimum" vsizetype="Preferred">
- <horstretch>0</horstretch>
- <verstretch>0</verstretch>
- </sizepolicy>
- </property>
- <property name="text">
- <string>Filter</string>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QLabel" name="currentCategoryLabel">
- <property name="font">
- <font>
- <pointsize>8</pointsize>
- <italic>true</italic>
- </font>
- </property>
- </widget>
- </item>
- <item>
- <spacer name="horizontalSpacer_5">
- <property name="orientation">
- <enum>Qt::Horizontal</enum>
- </property>
- <property name="sizeHint" stdset="0">
- <size>
- <width>40</width>
- <height>20</height>
- </size>
- </property>
- </spacer>
- </item>
- <item alignment="Qt::AlignLeft">
- <widget class="QPushButton" name="clearFiltersButton">
- <property name="sizePolicy">
- <sizepolicy hsizetype="Preferred" vsizetype="Maximum">
+ <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
- <width>0</width>
- <height>22</height>
+ <width>120</width>
+ <height>0</height>
</size>
</property>
- <property name="baseSize">
+ <property name="maximumSize">
<size>
- <width>95</width>
- <height>0</height>
+ <width>16777215</width>
+ <height>16777215</height>
</size>
</property>
- <property name="visible">
- <bool>false</bool>
+ <property name="font">
+ <font>
+ <pointsize>10</pointsize>
+ <weight>75</weight>
+ <bold>true</bold>
+ </font>
+ </property>
+ <property name="toolTip">
+ <string>Run program</string>
</property>
- <property name="layoutDirection">
- <enum>Qt::RightToLeft</enum>
+ <property name="whatsThis">
+ <string>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;
+&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;
+p, li { white-space: pre-wrap; }
+&lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:'MS Shell Dlg 2'; font-size:8.25pt; font-weight:400; font-style:normal;&quot;&gt;
+&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-size:8pt;&quot;&gt;Run the selected program with ModOrganizer enabled.&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
- <property name="styleSheet">
- <string notr="true">border:1px solid #ff0000;</string>
+ <property name="locale">
+ <locale language="English" country="UnitedStates"/>
</property>
<property name="text">
- <string>Clear all Filters</string>
+ <string>Run</string>
</property>
<property name="icon">
<iconset resource="resources.qrc">
- <normaloff>:/MO/gui/edit_clear</normaloff>:/MO/gui/edit_clear</iconset>
+ <normaloff>:/MO/gui/run</normaloff>:/MO/gui/run</iconset>
</property>
<property name="iconSize">
<size>
- <width>12</width>
- <height>12</height>
+ <width>36</width>
+ <height>36</height>
</size>
</property>
</widget>
</item>
<item>
- <widget class="QComboBox" name="groupCombo">
- <property name="baseSize">
+ <widget class="QPushButton" name="linkButton">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="minimumSize">
<size>
- <width>220</width>
+ <width>140</width>
<height>0</height>
</size>
</property>
- <property name="focusPolicy">
- <enum>Qt::ClickFocus</enum>
+ <property name="maximumSize">
+ <size>
+ <width>16777215</width>
+ <height>16777215</height>
+ </size>
</property>
- <item>
- <property name="text">
- <string>No groups</string>
- </property>
- </item>
- <item>
- <property name="text">
- <string>Categories</string>
- </property>
- </item>
- <item>
- <property name="text">
- <string>Nexus IDs</string>
- </property>
- </item>
- </widget>
- </item>
- <item>
- <widget class="MOBase::LineEditClear" name="modFilterEdit">
<property name="baseSize">
<size>
- <width>220</width>
+ <width>0</width>
<height>0</height>
</size>
</property>
- <property name="placeholderText">
- <string>Filter</string>
+ <property name="toolTip">
+ <string>Create a shortcut in your start menu or on the desktop to the specified program</string>
+ </property>
+ <property name="whatsThis">
+ <string>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;
+&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;
+p, li { white-space: pre-wrap; }
+&lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:'MS Shell Dlg 2'; font-size:8.25pt; font-weight:400; font-style:normal;&quot;&gt;
+&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-size:8pt;&quot;&gt;This creates a start menu shortcut that directly starts the selected program with the MO active.&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+ </property>
+ <property name="text">
+ <string>Shortcut</string>
+ </property>
+ <property name="icon">
+ <iconset resource="resources.qrc">
+ <normaloff>:/MO/gui/link</normaloff>:/MO/gui/link</iconset>
</property>
</widget>
</item>
@@ -560,748 +734,574 @@ p, li { white-space: pre-wrap; }
</item>
</layout>
</widget>
- <widget class="QWidget" name="layoutWidget_2">
- <layout class="QVBoxLayout" name="verticalLayout_2">
- <item>
- <widget class="QFrame" name="startGroup">
- <layout class="QHBoxLayout" name="horizontalLayout_5" stretch="1,0">
+ </item>
+ <item>
+ <widget class="QTabWidget" name="tabWidget">
+ <property name="minimumSize">
+ <size>
+ <width>340</width>
+ <height>250</height>
+ </size>
+ </property>
+ <property name="maximumSize">
+ <size>
+ <width>16777215</width>
+ <height>16777215</height>
+ </size>
+ </property>
+ <property name="contextMenuPolicy">
+ <enum>Qt::NoContextMenu</enum>
+ </property>
+ <property name="tabShape">
+ <enum>QTabWidget::Rounded</enum>
+ </property>
+ <property name="currentIndex">
+ <number>0</number>
+ </property>
+ <widget class="QWidget" name="espTab">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Expanding">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="maximumSize">
+ <size>
+ <width>16777215</width>
+ <height>16777215</height>
+ </size>
+ </property>
+ <attribute name="title">
+ <string>Plugins</string>
+ </attribute>
+ <layout class="QVBoxLayout" name="verticalLayout_4">
+ <property name="leftMargin">
+ <number>6</number>
+ </property>
+ <property name="topMargin">
+ <number>6</number>
+ </property>
+ <property name="rightMargin">
+ <number>6</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_7">
<item>
- <widget class="QComboBox" name="executablesListBox">
- <property name="sizePolicy">
- <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
- <horstretch>0</horstretch>
- <verstretch>0</verstretch>
- </sizepolicy>
+ <widget class="QPushButton" name="bossButton">
+ <property name="text">
+ <string>Sort</string>
</property>
- <property name="minimumSize">
+ <property name="icon">
+ <iconset resource="resources.qrc">
+ <normaloff>:/MO/gui/sort</normaloff>:/MO/gui/sort</iconset>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer_2">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
<size>
- <width>0</width>
- <height>40</height>
+ <width>40</width>
+ <height>20</height>
</size>
</property>
- <property name="font">
- <font>
- <pointsize>9</pointsize>
- <weight>75</weight>
- <bold>true</bold>
- </font>
- </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QPushButton" name="restoreButton">
<property name="toolTip">
- <string>Pick a program to run.</string>
+ <string>Restore Backup...</string>
</property>
- <property name="whatsThis">
- <string>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;
-&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;
-p, li { white-space: pre-wrap; }
-&lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:'MS Shell Dlg 2'; font-size:8.25pt; font-weight:400; font-style:normal;&quot;&gt;
-&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-size:8pt;&quot;&gt;Choose the program to run. Once you start using ModOrganizer, you should always run your game and tools from here or through shortcuts created here, otherwise mods installed through MO will not be visible.&lt;/span&gt;&lt;/p&gt;
-&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-size:8pt;&quot;&gt;You can add new Tools to this list, but I can't promise tools I haven't tested will work.&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+ <property name="text">
+ <string notr="true"/>
+ </property>
+ <property name="icon">
+ <iconset resource="resources.qrc">
+ <normaloff>:/MO/gui/restore</normaloff>:/MO/gui/restore</iconset>
</property>
<property name="iconSize">
<size>
- <width>32</width>
- <height>32</height>
+ <width>16</width>
+ <height>16</height>
</size>
</property>
- <property name="frame">
- <bool>false</bool>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="saveButton">
+ <property name="toolTip">
+ <string>Create Backup</string>
+ </property>
+ <property name="text">
+ <string notr="true"/>
+ </property>
+ <property name="icon">
+ <iconset resource="resources.qrc">
+ <normaloff>:/MO/gui/backup</normaloff>:/MO/gui/backup</iconset>
</property>
</widget>
</item>
<item>
- <layout class="QVBoxLayout" name="verticalLayout_12" stretch="0,0">
- <item>
- <widget class="QPushButton" name="startButton">
- <property name="sizePolicy">
- <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
- <horstretch>0</horstretch>
- <verstretch>0</verstretch>
- </sizepolicy>
- </property>
- <property name="minimumSize">
- <size>
- <width>120</width>
- <height>0</height>
- </size>
- </property>
- <property name="maximumSize">
- <size>
- <width>16777215</width>
- <height>16777215</height>
- </size>
- </property>
- <property name="font">
- <font>
- <pointsize>10</pointsize>
- <weight>75</weight>
- <bold>true</bold>
- </font>
- </property>
- <property name="toolTip">
- <string>Run program</string>
- </property>
- <property name="whatsThis">
- <string>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;
-&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;
-p, li { white-space: pre-wrap; }
-&lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:'MS Shell Dlg 2'; font-size:8.25pt; font-weight:400; font-style:normal;&quot;&gt;
-&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-size:8pt;&quot;&gt;Run the selected program with ModOrganizer enabled.&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
- </property>
- <property name="locale">
- <locale language="English" country="UnitedStates"/>
- </property>
- <property name="text">
- <string>Run</string>
- </property>
- <property name="icon">
- <iconset resource="resources.qrc">
- <normaloff>:/MO/gui/run</normaloff>:/MO/gui/run</iconset>
- </property>
- <property name="iconSize">
- <size>
- <width>36</width>
- <height>36</height>
- </size>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QPushButton" name="linkButton">
- <property name="sizePolicy">
- <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
- <horstretch>0</horstretch>
- <verstretch>0</verstretch>
- </sizepolicy>
- </property>
- <property name="minimumSize">
- <size>
- <width>140</width>
- <height>0</height>
- </size>
- </property>
- <property name="maximumSize">
- <size>
- <width>16777215</width>
- <height>16777215</height>
- </size>
- </property>
- <property name="baseSize">
- <size>
- <width>0</width>
- <height>0</height>
- </size>
- </property>
- <property name="toolTip">
- <string>Create a shortcut in your start menu or on the desktop to the specified program</string>
- </property>
- <property name="whatsThis">
- <string>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;
-&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;
-p, li { white-space: pre-wrap; }
-&lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:'MS Shell Dlg 2'; font-size:8.25pt; font-weight:400; font-style:normal;&quot;&gt;
-&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-size:8pt;&quot;&gt;This creates a start menu shortcut that directly starts the selected program with the MO active.&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
- </property>
- <property name="text">
- <string>Shortcut</string>
- </property>
- <property name="icon">
- <iconset resource="resources.qrc">
- <normaloff>:/MO/gui/link</normaloff>:/MO/gui/link</iconset>
- </property>
- </widget>
- </item>
- </layout>
+ <widget class="QLabel" name="activePluginsLabel">
+ <property name="text">
+ <string>Active:</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="LCDNumber" name="activePluginsCounter">
+ <property name="minimumSize">
+ <size>
+ <width>0</width>
+ <height>26</height>
+ </size>
+ </property>
+ <property name="whatsThis">
+ <string>This provides statistics about the plugin list. The total number of active plugins is normally displayed. Other statistics may be accessed with the tooltip of this counter.</string>
+ </property>
+ <property name="frameShadow">
+ <enum>QFrame::Sunken</enum>
+ </property>
+ <property name="digitCount">
+ <number>4</number>
+ </property>
+ <property name="segmentStyle">
+ <enum>QLCDNumber::Flat</enum>
+ </property>
+ </widget>
</item>
</layout>
- </widget>
- </item>
- <item>
- <widget class="QTabWidget" name="tabWidget">
- <property name="minimumSize">
- <size>
- <width>340</width>
- <height>250</height>
- </size>
- </property>
- <property name="maximumSize">
- <size>
- <width>16777215</width>
- <height>16777215</height>
- </size>
- </property>
- <property name="contextMenuPolicy">
- <enum>Qt::NoContextMenu</enum>
- </property>
- <property name="tabShape">
- <enum>QTabWidget::Rounded</enum>
- </property>
- <property name="currentIndex">
- <number>0</number>
- </property>
- <widget class="QWidget" name="espTab">
- <property name="sizePolicy">
- <sizepolicy hsizetype="Expanding" vsizetype="Expanding">
- <horstretch>0</horstretch>
- <verstretch>0</verstretch>
- </sizepolicy>
- </property>
- <property name="maximumSize">
+ </item>
+ <item>
+ <widget class="PluginListView" name="espList">
+ <property name="minimumSize">
<size>
- <width>16777215</width>
- <height>16777215</height>
+ <width>250</width>
+ <height>250</height>
</size>
</property>
- <attribute name="title">
- <string>Plugins</string>
- </attribute>
- <layout class="QVBoxLayout" name="verticalLayout_4">
- <property name="leftMargin">
- <number>6</number>
- </property>
- <property name="topMargin">
- <number>6</number>
- </property>
- <property name="rightMargin">
- <number>6</number>
- </property>
- <property name="bottomMargin">
- <number>0</number>
- </property>
- <item>
- <layout class="QHBoxLayout" name="horizontalLayout_7">
- <item>
- <widget class="QPushButton" name="bossButton">
- <property name="visible">
- <bool>true</bool>
- </property>
- <property name="text">
- <string>Sort</string>
- </property>
- <property name="icon">
- <iconset resource="resources.qrc">
- <normaloff>:/MO/gui/sort</normaloff>:/MO/gui/sort</iconset>
- </property>
- </widget>
- </item>
- <item>
- <spacer name="horizontalSpacer_2">
- <property name="orientation">
- <enum>Qt::Horizontal</enum>
- </property>
- <property name="sizeHint" stdset="0">
- <size>
- <width>40</width>
- <height>20</height>
- </size>
- </property>
- </spacer>
- </item>
- <item>
- <widget class="QPushButton" name="restoreButton">
- <property name="toolTip">
- <string>Restore Backup...</string>
- </property>
- <property name="text">
- <string notr="true"/>
- </property>
- <property name="icon">
- <iconset resource="resources.qrc">
- <normaloff>:/MO/gui/restore</normaloff>:/MO/gui/restore</iconset>
- </property>
- <property name="iconSize">
- <size>
- <width>16</width>
- <height>16</height>
- </size>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QPushButton" name="saveButton">
- <property name="toolTip">
- <string>Create Backup</string>
- </property>
- <property name="text">
- <string notr="true"/>
- </property>
- <property name="icon">
- <iconset resource="resources.qrc">
- <normaloff>:/MO/gui/backup</normaloff>:/MO/gui/backup</iconset>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QLabel" name="activePluginsLabel">
- <property name="text">
- <string>Active:</string>
- </property>
- </widget>
- </item>
- <item>
- <widget class="LCDNumber" name="activePluginsCounter">
- <property name="minimumSize">
- <size>
- <width>0</width>
- <height>26</height>
- </size>
- </property>
- <property name="whatsThis">
- <string>This provides statistics about the plugin list. The total number of active plugins is normally displayed. Other statistics may be accessed with the tooltip of this counter.</string>
- </property>
- <property name="frameShadow">
- <enum>QFrame::Sunken</enum>
- </property>
- <property name="digitCount">
- <number>4</number>
- </property>
- <property name="segmentStyle">
- <enum>QLCDNumber::Flat</enum>
- </property>
- </widget>
- </item>
- </layout>
- </item>
- <item>
- <widget class="PluginListView" name="espList">
- <property name="minimumSize">
- <size>
- <width>250</width>
- <height>250</height>
- </size>
- </property>
- <property name="contextMenuPolicy">
- <enum>Qt::CustomContextMenu</enum>
- </property>
- <property name="toolTip">
- <string>List of available esp/esm files</string>
- </property>
- <property name="whatsThis">
- <string>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;
+ <property name="contextMenuPolicy">
+ <enum>Qt::CustomContextMenu</enum>
+ </property>
+ <property name="toolTip">
+ <string>List of available esp/esm files</string>
+ </property>
+ <property name="whatsThis">
+ <string>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;
&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;
p, li { white-space: pre-wrap; }
&lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:'MS Shell Dlg 2'; font-size:8.25pt; font-weight:400; font-style:normal;&quot;&gt;
&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-size:8pt;&quot;&gt;This list contains the esps, esms, and esls contained in the active mods. These require their own load order. Use drag&amp;amp;drop to modify this load order. Please note that MO will only save the load order for mods that are active/checked.&lt;br /&gt;There is a great tool named &amp;quot;BOSS&amp;quot; to automatically sort these files.&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
- </property>
- <property name="editTriggers">
- <set>QAbstractItemView::EditKeyPressed|QAbstractItemView::SelectedClicked</set>
- </property>
- <property name="dragEnabled">
- <bool>true</bool>
- </property>
- <property name="dragDropOverwriteMode">
- <bool>false</bool>
- </property>
- <property name="dragDropMode">
- <enum>QAbstractItemView::InternalMove</enum>
- </property>
- <property name="defaultDropAction">
- <enum>Qt::MoveAction</enum>
- </property>
- <property name="alternatingRowColors">
- <bool>true</bool>
- </property>
- <property name="selectionMode">
- <enum>QAbstractItemView::ExtendedSelection</enum>
- </property>
- <property name="selectionBehavior">
- <enum>QAbstractItemView::SelectRows</enum>
- </property>
- <property name="indentation">
- <number>0</number>
- </property>
- <property name="uniformRowHeights">
- <bool>true</bool>
- </property>
- <property name="itemsExpandable">
- <bool>false</bool>
- </property>
- <property name="sortingEnabled">
- <bool>true</bool>
- </property>
- <property name="expandsOnDoubleClick">
- <bool>false</bool>
- </property>
- <attribute name="headerStretchLastSection">
- <bool>false</bool>
- </attribute>
- </widget>
- </item>
- <item>
- <layout class="QHBoxLayout" name="horizontalLayout_3">
- <item>
- <widget class="MOBase::LineEditClear" name="espFilterEdit">
- <property name="text">
- <string/>
- </property>
- <property name="placeholderText">
- <string>Filter</string>
- </property>
- </widget>
- </item>
- </layout>
- </item>
- </layout>
- </widget>
- <widget class="QWidget" name="bsaTab">
- <property name="visible">
+ </property>
+ <property name="editTriggers">
+ <set>QAbstractItemView::EditKeyPressed|QAbstractItemView::SelectedClicked</set>
+ </property>
+ <property name="dragEnabled">
+ <bool>true</bool>
+ </property>
+ <property name="dragDropOverwriteMode">
+ <bool>false</bool>
+ </property>
+ <property name="dragDropMode">
+ <enum>QAbstractItemView::InternalMove</enum>
+ </property>
+ <property name="defaultDropAction">
+ <enum>Qt::MoveAction</enum>
+ </property>
+ <property name="alternatingRowColors">
+ <bool>true</bool>
+ </property>
+ <property name="selectionMode">
+ <enum>QAbstractItemView::ExtendedSelection</enum>
+ </property>
+ <property name="selectionBehavior">
+ <enum>QAbstractItemView::SelectRows</enum>
+ </property>
+ <property name="indentation">
+ <number>0</number>
+ </property>
+ <property name="uniformRowHeights">
+ <bool>true</bool>
+ </property>
+ <property name="itemsExpandable">
+ <bool>false</bool>
+ </property>
+ <property name="sortingEnabled">
+ <bool>true</bool>
+ </property>
+ <property name="expandsOnDoubleClick">
<bool>false</bool>
</property>
- <attribute name="title">
- <string>Archives</string>
+ <attribute name="headerStretchLastSection">
+ <bool>false</bool>
</attribute>
- <layout class="QVBoxLayout" name="verticalLayout_9">
- <property name="leftMargin">
- <number>6</number>
- </property>
- <property name="topMargin">
- <number>6</number>
- </property>
- <property name="rightMargin">
- <number>6</number>
- </property>
- <property name="bottomMargin">
- <number>6</number>
- </property>
- <item>
- <layout class="QHBoxLayout" name="horizontalLayout_9" stretch="0">
- <item>
- <widget class="QLabel" name="managedArchiveLabel">
- <property name="toolTip">
- <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;BSAs / BA2s are bundles of game assets (textures, scripts, etc.). By default, the engine loads these bundles in a separate step from loose files. &lt;p&gt;Their load order is specified by the priority of the corresponding plugin (right pane, plugins tab).&lt;/p&gt;&lt;p&gt;If there is a matching plugin, the game will load them no matter what.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
- </property>
- <property name="text">
- <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Currently detected archives. (&lt;a href=&quot;#&quot;&gt;&lt;span style=&quot; text-decoration: underline; color:#0000ff;&quot;&gt;What is an archive?&lt;/span&gt;&lt;/a&gt;)&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
- </property>
- <property name="wordWrap">
- <bool>true</bool>
- </property>
- </widget>
- </item>
- </layout>
- </item>
- <item>
- <widget class="MOBase::SortableTreeWidget" name="bsaList" native="true">
- <property name="contextMenuPolicy">
- <enum>Qt::CustomContextMenu</enum>
- </property>
- <property name="toolTip">
- <string>List of available BS Archives. Archives not checked here are not managed by MO and ignore installation order.</string>
- </property>
- <property name="whatsThis">
- <string>BSA files are archives (comparable to .zip files) that contain data assets (meshes, textures, ...) to be used by the game. As such they &quot;compete&quot; with loose files in your data directory over which is loaded.
+ </widget>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_3">
+ <item>
+ <widget class="MOBase::LineEditClear" name="espFilterEdit">
+ <property name="text">
+ <string/>
+ </property>
+ <property name="placeholderText">
+ <string>Filter</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </widget>
+ <widget class="QWidget" name="bsaTab">
+ <property name="visible">
+ <bool>false</bool>
+ </property>
+ <attribute name="title">
+ <string>Archives</string>
+ </attribute>
+ <layout class="QVBoxLayout" name="verticalLayout_9">
+ <property name="leftMargin">
+ <number>6</number>
+ </property>
+ <property name="topMargin">
+ <number>6</number>
+ </property>
+ <property name="rightMargin">
+ <number>6</number>
+ </property>
+ <property name="bottomMargin">
+ <number>6</number>
+ </property>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_9" stretch="0">
+ <item>
+ <widget class="LinkLabel" name="managedArchiveLabel">
+ <property name="toolTip">
+ <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;BSAs / BA2s are bundles of game assets (textures, scripts, etc.). By default, the engine loads these bundles in a separate step from loose files. &lt;p&gt;Their load order is specified by the priority of the corresponding plugin (right pane, plugins tab).&lt;/p&gt;&lt;p&gt;If there is a matching plugin, the game will load them no matter what.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+ </property>
+ <property name="text">
+ <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Currently detected archives. (&lt;a href=&quot;#&quot;&gt;What is an archive?&lt;/a&gt;)&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+ </property>
+ <property name="wordWrap">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <widget class="MOBase::SortableTreeWidget" name="bsaList" native="true">
+ <property name="contextMenuPolicy">
+ <enum>Qt::CustomContextMenu</enum>
+ </property>
+ <property name="toolTip">
+ <string>List of available BS Archives. Archives not checked here are not managed by MO and ignore installation order.</string>
+ </property>
+ <property name="whatsThis">
+ <string>BSA files are archives (comparable to .zip files) that contain data assets (meshes, textures, ...) to be used by the game. As such they &quot;compete&quot; with loose files in your data directory over which is loaded.
By default, BSAs that share their base name with an enabled ESP (i.e. plugin.esp and plugin.bsa) are automatically loaded and will have precedence over all loose files, the installation order you set up to the left is then ignored!
BSAs checked here are loaded in such a way that your installation order is obeyed properly.</string>
- </property>
- <property name="showDropIndicator" stdset="0">
- <bool>false</bool>
- </property>
- <property name="dragEnabled" stdset="0">
- <bool>false</bool>
- </property>
- <property name="dragDropOverwriteMode" stdset="0">
- <bool>false</bool>
- </property>
- <property name="indentation" stdset="0">
- <number>20</number>
- </property>
- <property name="itemsExpandable" stdset="0">
- <bool>true</bool>
- </property>
- <property name="columnCount" stdset="0">
- <number>1</number>
- </property>
- </widget>
- </item>
- </layout>
+ </property>
+ <property name="showDropIndicator" stdset="0">
+ <bool>false</bool>
+ </property>
+ <property name="dragEnabled" stdset="0">
+ <bool>false</bool>
+ </property>
+ <property name="dragDropOverwriteMode" stdset="0">
+ <bool>false</bool>
+ </property>
+ <property name="indentation" stdset="0">
+ <number>20</number>
+ </property>
+ <property name="itemsExpandable" stdset="0">
+ <bool>true</bool>
+ </property>
+ <property name="columnCount" stdset="0">
+ <number>1</number>
+ </property>
</widget>
- <widget class="QWidget" name="dataTab">
- <attribute name="title">
- <string>Data</string>
- </attribute>
- <layout class="QVBoxLayout" name="verticalLayout_5">
- <property name="leftMargin">
- <number>6</number>
- </property>
- <property name="topMargin">
- <number>6</number>
- </property>
- <property name="rightMargin">
- <number>6</number>
- </property>
- <property name="bottomMargin">
- <number>6</number>
- </property>
- <item>
- <widget class="QPushButton" name="btnRefreshData">
- <property name="toolTip">
- <string>refresh data-directory overview</string>
- </property>
- <property name="whatsThis">
- <string>Refresh the overview. This may take a moment.</string>
- </property>
- <property name="text">
- <string>Refresh</string>
- </property>
- <property name="icon">
- <iconset resource="resources.qrc">
- <normaloff>:/MO/gui/resources/view-refresh.png</normaloff>:/MO/gui/resources/view-refresh.png</iconset>
- </property>
- </widget>
- </item>
- <item>
- <layout class="QHBoxLayout" name="horizontalLayout_2">
- <item>
- <widget class="QTreeWidget" name="dataTree">
- <property name="contextMenuPolicy">
- <enum>Qt::CustomContextMenu</enum>
- </property>
- <property name="whatsThis">
- <string>This is an overview of your data directory as visible to the game (and tools). </string>
- </property>
- <property name="uniformRowHeights">
- <bool>true</bool>
- </property>
- <property name="animated">
- <bool>true</bool>
- </property>
- <attribute name="headerDefaultSectionSize">
- <number>400</number>
- </attribute>
- <column>
- <property name="text">
- <string>File</string>
- </property>
- </column>
- <column>
- <property name="text">
- <string>Mod</string>
- </property>
- </column>
- </widget>
- </item>
- </layout>
- </item>
- <item>
- <layout class="QHBoxLayout" name="horizontalLayout_12">
- <item>
- <widget class="QCheckBox" name="conflictsCheckBox">
- <property name="toolTip">
- <string>Filters the above list so that only conflicts are displayed.</string>
- </property>
- <property name="whatsThis">
- <string>Filters the above list so that only conflicts are displayed.</string>
- </property>
- <property name="text">
- <string>Show only conflicts</string>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QCheckBox" name="showArchiveDataCheckBox">
- <property name="toolTip">
- <string>Filters the above list so that files from archives are not shown</string>
- </property>
- <property name="statusTip">
- <string/>
- </property>
- <property name="whatsThis">
- <string>Filters the above list so that files from archives are not shown</string>
- </property>
- <property name="text">
- <string>Show files from Archives</string>
- </property>
- </widget>
- </item>
- </layout>
- </item>
- </layout>
+ </item>
+ </layout>
+ </widget>
+ <widget class="QWidget" name="dataTab">
+ <attribute name="title">
+ <string>Data</string>
+ </attribute>
+ <layout class="QVBoxLayout" name="verticalLayout_5">
+ <property name="leftMargin">
+ <number>6</number>
+ </property>
+ <property name="topMargin">
+ <number>6</number>
+ </property>
+ <property name="rightMargin">
+ <number>6</number>
+ </property>
+ <property name="bottomMargin">
+ <number>6</number>
+ </property>
+ <item>
+ <widget class="QPushButton" name="btnRefreshData">
+ <property name="toolTip">
+ <string>refresh data-directory overview</string>
+ </property>
+ <property name="whatsThis">
+ <string>Refresh the overview. This may take a moment.</string>
+ </property>
+ <property name="text">
+ <string>Refresh</string>
+ </property>
+ <property name="icon">
+ <iconset resource="resources.qrc">
+ <normaloff>:/MO/gui/resources/view-refresh.png</normaloff>:/MO/gui/resources/view-refresh.png</iconset>
+ </property>
</widget>
- <widget class="QWidget" name="savesTab">
- <attribute name="title">
- <string>Saves</string>
- </attribute>
- <layout class="QVBoxLayout" name="verticalLayout_3">
- <property name="leftMargin">
- <number>6</number>
- </property>
- <property name="topMargin">
- <number>6</number>
- </property>
- <property name="rightMargin">
- <number>6</number>
- </property>
- <property name="bottomMargin">
- <number>6</number>
- </property>
- <item>
- <widget class="QListWidget" name="savegameList">
- <property name="contextMenuPolicy">
- <enum>Qt::CustomContextMenu</enum>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_2">
+ <item>
+ <widget class="QTreeWidget" name="dataTree">
+ <property name="contextMenuPolicy">
+ <enum>Qt::CustomContextMenu</enum>
+ </property>
+ <property name="whatsThis">
+ <string>This is an overview of your data directory as visible to the game (and tools). </string>
+ </property>
+ <property name="alternatingRowColors">
+ <bool>true</bool>
+ </property>
+ <property name="uniformRowHeights">
+ <bool>true</bool>
+ </property>
+ <attribute name="headerDefaultSectionSize">
+ <number>400</number>
+ </attribute>
+ <column>
+ <property name="text">
+ <string>File</string>
</property>
- <property name="toolTip">
- <string notr="true"/>
+ </column>
+ <column>
+ <property name="text">
+ <string>Mod</string>
</property>
- <property name="whatsThis">
- <string>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;
+ </column>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_12">
+ <item>
+ <widget class="QCheckBox" name="conflictsCheckBox">
+ <property name="toolTip">
+ <string>Filters the above list so that only conflicts are displayed.</string>
+ </property>
+ <property name="whatsThis">
+ <string>Filters the above list so that only conflicts are displayed.</string>
+ </property>
+ <property name="text">
+ <string>Show only conflicts</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="showArchiveDataCheckBox">
+ <property name="toolTip">
+ <string>Filters the above list so that files from archives are not shown</string>
+ </property>
+ <property name="statusTip">
+ <string/>
+ </property>
+ <property name="whatsThis">
+ <string>Filters the above list so that files from archives are not shown</string>
+ </property>
+ <property name="text">
+ <string>Show files from Archives</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </widget>
+ <widget class="QWidget" name="savesTab">
+ <attribute name="title">
+ <string>Saves</string>
+ </attribute>
+ <layout class="QVBoxLayout" name="verticalLayout_3">
+ <property name="leftMargin">
+ <number>6</number>
+ </property>
+ <property name="topMargin">
+ <number>6</number>
+ </property>
+ <property name="rightMargin">
+ <number>6</number>
+ </property>
+ <property name="bottomMargin">
+ <number>6</number>
+ </property>
+ <item>
+ <widget class="QListWidget" name="savegameList">
+ <property name="contextMenuPolicy">
+ <enum>Qt::CustomContextMenu</enum>
+ </property>
+ <property name="toolTip">
+ <string notr="true"/>
+ </property>
+ <property name="whatsThis">
+ <string>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;
&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;
p, li { white-space: pre-wrap; }
&lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:'MS Shell Dlg 2'; font-size:8.25pt; font-weight:400; font-style:normal;&quot;&gt;
&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-size:8pt;&quot;&gt;This is a list of all save games for this game. Hover over a list entry to get detailed information about the save including a list of esps/esms that were used at the time this save was created but aren't active now.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-size:8pt;&quot;&gt;&lt;/p&gt;
&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-size:8pt;&quot;&gt;If you click &amp;quot;Fix Mods...&amp;quot; in the context menu, MO will try to activate all mods and esps to fix those missing esps. It will not disable anything!&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
- </property>
- <property name="selectionMode">
- <enum>QAbstractItemView::ExtendedSelection</enum>
- </property>
- <property name="selectionBehavior">
- <enum>QAbstractItemView::SelectRows</enum>
- </property>
- </widget>
- </item>
- </layout>
+ </property>
+ <property name="alternatingRowColors">
+ <bool>true</bool>
+ </property>
+ <property name="selectionMode">
+ <enum>QAbstractItemView::ExtendedSelection</enum>
+ </property>
+ <property name="selectionBehavior">
+ <enum>QAbstractItemView::SelectRows</enum>
+ </property>
</widget>
- <widget class="QWidget" name="downloadTab">
- <attribute name="title">
- <string>Downloads</string>
- </attribute>
- <layout class="QVBoxLayout" name="verticalLayout_7">
- <property name="leftMargin">
- <number>2</number>
- </property>
- <property name="topMargin">
- <number>2</number>
- </property>
- <property name="rightMargin">
- <number>2</number>
- </property>
- <property name="bottomMargin">
- <number>2</number>
- </property>
- <item>
- <widget class="QPushButton" name="btnRefreshDownloads">
- <property name="toolTip">
- <string>Refresh downloads view</string>
- </property>
- <property name="text">
- <string>Refresh</string>
- </property>
- <property name="icon">
- <iconset resource="resources.qrc">
- <normaloff>:/MO/gui/resources/view-refresh.png</normaloff>:/MO/gui/resources/view-refresh.png</iconset>
- </property>
- </widget>
- </item>
- <item>
- <layout class="QVBoxLayout" name="downloadLayout">
- <item>
- <widget class="DownloadListWidget" name="downloadView">
- <property name="minimumSize">
- <size>
- <width>320</width>
- <height>0</height>
- </size>
- </property>
- <property name="contextMenuPolicy">
- <enum>Qt::CustomContextMenu</enum>
- </property>
- <property name="acceptDrops">
- <bool>true</bool>
- </property>
- <property name="toolTip">
- <string/>
- </property>
- <property name="whatsThis">
- <string>This is a list of mods you downloaded from Nexus. Double click one to install it. You can also drag an archive into here.</string>
- </property>
- <property name="verticalScrollBarPolicy">
- <enum>Qt::ScrollBarAlwaysOn</enum>
- </property>
- <property name="dragEnabled">
- <bool>true</bool>
- </property>
- <property name="dragDropMode">
- <enum>QAbstractItemView::DragDrop</enum>
- </property>
- <property name="defaultDropAction">
- <enum>Qt::MoveAction</enum>
- </property>
- <property name="alternatingRowColors">
- <bool>true</bool>
- </property>
- <property name="verticalScrollMode">
- <enum>QAbstractItemView::ScrollPerPixel</enum>
- </property>
- <property name="indentation">
- <number>0</number>
- </property>
- <property name="itemsExpandable">
- <bool>false</bool>
- </property>
- <property name="sortingEnabled">
- <bool>true</bool>
- </property>
- </widget>
- </item>
- </layout>
- </item>
- <item>
- <layout class="QHBoxLayout" name="horizontalLayout" stretch="0,0,2">
- <item>
- <widget class="QCheckBox" name="showHiddenBox">
- <property name="text">
- <string>Show Hidden</string>
- </property>
- </widget>
- </item>
- <item>
- <spacer name="horizontalSpacer_3">
- <property name="orientation">
- <enum>Qt::Horizontal</enum>
- </property>
- <property name="sizeHint" stdset="0">
- <size>
- <width>40</width>
- <height>20</height>
- </size>
- </property>
- </spacer>
- </item>
- <item>
- <widget class="MOBase::LineEditClear" name="downloadFilterEdit">
- <property name="placeholderText">
- <string>Filter</string>
- </property>
- </widget>
- </item>
- </layout>
- </item>
- </layout>
+ </item>
+ </layout>
+ </widget>
+ <widget class="QWidget" name="downloadTab">
+ <attribute name="title">
+ <string>Downloads</string>
+ </attribute>
+ <layout class="QVBoxLayout" name="verticalLayout_7">
+ <property name="leftMargin">
+ <number>2</number>
+ </property>
+ <property name="topMargin">
+ <number>2</number>
+ </property>
+ <property name="rightMargin">
+ <number>2</number>
+ </property>
+ <property name="bottomMargin">
+ <number>2</number>
+ </property>
+ <item>
+ <widget class="QPushButton" name="btnRefreshDownloads">
+ <property name="toolTip">
+ <string>Refresh downloads view</string>
+ </property>
+ <property name="text">
+ <string>Refresh</string>
+ </property>
+ <property name="icon">
+ <iconset resource="resources.qrc">
+ <normaloff>:/MO/gui/resources/view-refresh.png</normaloff>:/MO/gui/resources/view-refresh.png</iconset>
+ </property>
</widget>
- </widget>
- </item>
- </layout>
+ </item>
+ <item>
+ <layout class="QVBoxLayout" name="downloadLayout">
+ <item>
+ <widget class="DownloadListWidget" name="downloadView">
+ <property name="minimumSize">
+ <size>
+ <width>320</width>
+ <height>0</height>
+ </size>
+ </property>
+ <property name="contextMenuPolicy">
+ <enum>Qt::CustomContextMenu</enum>
+ </property>
+ <property name="acceptDrops">
+ <bool>true</bool>
+ </property>
+ <property name="toolTip">
+ <string/>
+ </property>
+ <property name="whatsThis">
+ <string>This is a list of mods you downloaded from Nexus. Double click one to install it. You can also drag an archive into here.</string>
+ </property>
+ <property name="verticalScrollBarPolicy">
+ <enum>Qt::ScrollBarAlwaysOn</enum>
+ </property>
+ <property name="dragEnabled">
+ <bool>true</bool>
+ </property>
+ <property name="dragDropMode">
+ <enum>QAbstractItemView::DragDrop</enum>
+ </property>
+ <property name="defaultDropAction">
+ <enum>Qt::MoveAction</enum>
+ </property>
+ <property name="alternatingRowColors">
+ <bool>true</bool>
+ </property>
+ <property name="verticalScrollMode">
+ <enum>QAbstractItemView::ScrollPerPixel</enum>
+ </property>
+ <property name="indentation">
+ <number>0</number>
+ </property>
+ <property name="itemsExpandable">
+ <bool>false</bool>
+ </property>
+ <property name="sortingEnabled">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout" stretch="0,0,2">
+ <item>
+ <widget class="QCheckBox" name="showHiddenBox">
+ <property name="text">
+ <string>Show Hidden</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer_3">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>40</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="MOBase::LineEditClear" name="downloadFilterEdit">
+ <property name="placeholderText">
+ <string>Filter</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </widget>
</widget>
- </widget>
- </item>
- </layout>
- </widget>
- <widget class="QTreeView" name="logList">
- <property name="contextMenuPolicy">
- <enum>Qt::ActionsContextMenu</enum>
- </property>
- <property name="selectionMode">
- <enum>QAbstractItemView::NoSelection</enum>
- </property>
- <property name="uniformRowHeights">
- <bool>true</bool>
- </property>
- <property name="itemsExpandable">
- <bool>false</bool>
- </property>
- <property name="headerHidden">
- <bool>true</bool>
- </property>
+ </item>
+ </layout>
+ </widget>
</widget>
</widget>
</item>
@@ -1342,7 +1342,7 @@ p, li { white-space: pre-wrap; }
<addaction name="actionUpdate"/>
<addaction name="actionHelp"/>
</widget>
- <widget class="QStatusBar" name="statusBar"/>
+ <widget class="StatusBar" name="statusBar"/>
<widget class="QMenuBar" name="menuBar">
<property name="geometry">
<rect>
@@ -1403,6 +1403,7 @@ p, li { white-space: pre-wrap; }
<addaction name="actionToolBarIconsAndText"/>
</widget>
<addaction name="menuToolbars"/>
+ <addaction name="actionViewLog"/>
<addaction name="separator"/>
<addaction name="actionNotifications"/>
</widget>
@@ -1417,6 +1418,49 @@ p, li { white-space: pre-wrap; }
<addaction name="menuRun"/>
<addaction name="menuHelp"/>
</widget>
+ <widget class="QDockWidget" name="logDock">
+ <property name="features">
+ <set>QDockWidget::AllDockWidgetFeatures</set>
+ </property>
+ <property name="windowTitle">
+ <string>Log</string>
+ </property>
+ <attribute name="dockWidgetArea">
+ <number>8</number>
+ </attribute>
+ <widget class="QWidget" name="dockWidgetContents">
+ <layout class="QVBoxLayout" name="verticalLayout_6">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="LogList" name="logList">
+ <property name="contextMenuPolicy">
+ <enum>Qt::CustomContextMenu</enum>
+ </property>
+ <property name="rootIsDecorated">
+ <bool>false</bool>
+ </property>
+ <property name="uniformRowHeights">
+ <bool>true</bool>
+ </property>
+ <attribute name="headerVisible">
+ <bool>false</bool>
+ </attribute>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </widget>
<action name="actionInstallMod">
<property name="icon">
<iconset resource="resources.qrc">
@@ -1618,20 +1662,6 @@ p, li { white-space: pre-wrap; }
<string>Endorse Mod Organizer</string>
</property>
</action>
- <action name="actionCopy_Log_to_Clipboard">
- <property name="text">
- <string>Copy &amp;Log</string>
- </property>
- <property name="iconText">
- <string>Copy &amp;Log</string>
- </property>
- <property name="toolTip">
- <string>Copy log to clipboard</string>
- </property>
- <property name="statusTip">
- <string>Copy log to clipboard</string>
- </property>
- </action>
<action name="actionChange_Game">
<property name="icon">
<iconset resource="resources.qrc">
@@ -1736,6 +1766,14 @@ p, li { white-space: pre-wrap; }
<string>Status &amp;bar</string>
</property>
</action>
+ <action name="actionViewLog">
+ <property name="checkable">
+ <bool>true</bool>
+ </property>
+ <property name="text">
+ <string>Log</string>
+ </property>
+ </action>
</widget>
<layoutdefault spacing="6" margin="11"/>
<customwidgets>
@@ -1769,6 +1807,21 @@ p, li { white-space: pre-wrap; }
<extends>QWidget</extends>
<header>sortabletreewidget.h</header>
</customwidget>
+ <customwidget>
+ <class>LogList</class>
+ <extends>QTreeView</extends>
+ <header>loglist.h</header>
+ </customwidget>
+ <customwidget>
+ <class>StatusBar</class>
+ <extends>QStatusBar</extends>
+ <header>statusbar.h</header>
+ </customwidget>
+ <customwidget>
+ <class>LinkLabel</class>
+ <extends>QLabel</extends>
+ <header location="global">linklabel.h</header>
+ </customwidget>
</customwidgets>
<resources>
<include location="resources.qrc"/>
diff --git a/src/messagedialog.cpp b/src/messagedialog.cpp
index 6c6de3e7..78a5dd4d 100644
--- a/src/messagedialog.cpp
+++ b/src/messagedialog.cpp
@@ -19,10 +19,13 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#include "messagedialog.h"
#include "ui_messagedialog.h"
+#include <log.h>
#include <QTimer>
#include <QResizeEvent>
#include <Windows.h>
+using namespace MOBase;
+
MessageDialog::MessageDialog(const QString &text, QWidget *reference) :
QDialog(reference),
ui(new Ui::MessageDialog)
@@ -81,7 +84,8 @@ void MessageDialog::resizeEvent(QResizeEvent *event)
void MessageDialog::showMessage(const QString &text, QWidget *reference, bool bringToFront)
{
- qDebug("%s", qUtf8Printable(text));
+ log::debug("{}", text);
+
if (reference != nullptr) {
if (bringToFront || (qApp->activeWindow() != nullptr)) {
MessageDialog *dialog = new MessageDialog(text, reference);
diff --git a/src/moapplication.cpp b/src/moapplication.cpp
index 5652833a..a071d58b 100644
--- a/src/moapplication.cpp
+++ b/src/moapplication.cpp
@@ -20,6 +20,7 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#include "moapplication.h"
#include <report.h>
#include <utility.h>
+#include <log.h>
#include <appconfig.h>
#include <QFile>
#include <QStringList>
@@ -36,7 +37,7 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#include <QDebug>
-using MOBase::reportError;
+using namespace MOBase;
class ProxyStyle : public QProxyStyle {
@@ -50,7 +51,7 @@ public:
if(element == QStyle::PE_IndicatorItemViewItemDrop) {
painter->setRenderHint(QPainter::Antialiasing, true);
- QColor col(option->palette.foreground().color());
+ QColor col(option->palette.windowText().color());
QPen pen(col);
pen.setWidth(2);
col.setAlpha(50);
@@ -114,13 +115,15 @@ bool MOApplication::notify(QObject *receiver, QEvent *event)
try {
return QApplication::notify(receiver, event);
} catch (const std::exception &e) {
- qCritical("uncaught exception in handler (object %s, eventtype %d): %s",
- receiver->objectName().toUtf8().constData(), event->type(), e.what());
+ log::error(
+ "uncaught exception in handler (object {}, eventtype {}): {}",
+ receiver->objectName(), event->type(), e.what());
reportError(tr("an error occurred: %1").arg(e.what()));
return false;
} catch (...) {
- qCritical("uncaught non-std exception in handler (object %s, eventtype %d)",
- receiver->objectName().toUtf8().constData(), event->type());
+ log::error(
+ "uncaught non-std exception in handler (object {}, eventtype {})",
+ receiver->objectName(), event->type());
reportError(tr("an error occurred"));
return false;
}
@@ -129,15 +132,15 @@ bool MOApplication::notify(QObject *receiver, QEvent *event)
void MOApplication::updateStyle(const QString &fileName)
{
- if (fileName == "Fusion") {
- setStyle(QStyleFactory::create("fusion"));
+ if (QStyleFactory::keys().contains(fileName)) {
+ setStyle(QStyleFactory::create(fileName));
setStyleSheet("");
} else {
setStyle(new ProxyStyle(QStyleFactory::create(m_DefaultStyle)));
if (QFile::exists(fileName)) {
setStyleSheet(QString("file:///%1").arg(fileName));
} else {
- qWarning("invalid stylesheet: %s", qUtf8Printable(fileName));
+ log::warn("invalid stylesheet: {}", fileName);
}
}
}
diff --git a/src/modconflicticondelegate.cpp b/src/modconflicticondelegate.cpp
new file mode 100644
index 00000000..2ccf3363
--- /dev/null
+++ b/src/modconflicticondelegate.cpp
@@ -0,0 +1,160 @@
+#include "modconflicticondelegate.h"
+#include <log.h>
+#include <QList>
+
+using namespace MOBase;
+
+ModInfo::EConflictFlag ModConflictIconDelegate::m_ConflictFlags[4] = { ModInfo::FLAG_CONFLICT_MIXED
+ , ModInfo::FLAG_CONFLICT_OVERWRITE
+ , ModInfo::FLAG_CONFLICT_OVERWRITTEN
+ , ModInfo::FLAG_CONFLICT_REDUNDANT };
+
+ModInfo::EConflictFlag ModConflictIconDelegate::m_ArchiveLooseConflictFlags[2] = { ModInfo::FLAG_ARCHIVE_LOOSE_CONFLICT_OVERWRITE
+ , ModInfo::FLAG_ARCHIVE_LOOSE_CONFLICT_OVERWRITTEN };
+
+ModInfo::EConflictFlag ModConflictIconDelegate::m_ArchiveConflictFlags[3] = { ModInfo::FLAG_ARCHIVE_CONFLICT_MIXED
+ , ModInfo::FLAG_ARCHIVE_CONFLICT_OVERWRITE
+ , ModInfo::FLAG_ARCHIVE_CONFLICT_OVERWRITTEN };
+
+ModConflictIconDelegate::ModConflictIconDelegate(QObject *parent, int logicalIndex, int compactSize)
+ : IconDelegate(parent)
+ , m_LogicalIndex(logicalIndex)
+ , m_CompactSize(compactSize)
+ , m_Compact(false)
+{
+}
+
+void ModConflictIconDelegate::columnResized(int logicalIndex, int, int newSize)
+{
+ if (logicalIndex == m_LogicalIndex) {
+ m_Compact = newSize < m_CompactSize;
+ }
+}
+
+QList<QString> ModConflictIconDelegate::getIconsForFlags(
+ std::vector<ModInfo::EConflictFlag> flags, bool compact)
+{
+ QList<QString> result;
+
+ // Don't do flags for overwrite
+ if (std::find(flags.begin(), flags.end(),ModInfo::FLAG_OVERWRITE_CONFLICT) != flags.end())
+ return result;
+
+ // insert conflict icons to provide nicer alignment
+ { // insert loose file conflicts first
+ auto iter = std::find_first_of(flags.begin(), flags.end(),
+ m_ConflictFlags, m_ConflictFlags + 4);
+ if (iter != flags.end()) {
+ result.append(getFlagIcon(*iter));
+ flags.erase(iter);
+ } else if (!compact) {
+ result.append(QString());
+ }
+ }
+
+ { // insert loose vs archive overwrite second
+ auto iter = std::find(flags.begin(), flags.end(),
+ ModInfo::FLAG_ARCHIVE_LOOSE_CONFLICT_OVERWRITE);
+ if (iter != flags.end()) {
+ result.append(getFlagIcon(*iter));
+ flags.erase(iter);
+ } else if (!compact) {
+ result.append(QString());
+ }
+ }
+
+ { // insert loose vs archive overwritten third
+ auto iter = std::find_first_of(flags.begin(), flags.end(),
+ m_ArchiveLooseConflictFlags + 1, m_ArchiveLooseConflictFlags + 2);
+ if (iter != flags.end()) {
+ result.append(getFlagIcon(*iter));
+ flags.erase(iter);
+ } else if (!compact) {
+ result.append(QString());
+ }
+ }
+
+ { // insert archive conflicts last
+ auto iter = std::find_first_of(flags.begin(), flags.end(),
+ m_ArchiveConflictFlags, m_ArchiveConflictFlags + 3);
+ if (iter != flags.end()) {
+ result.append(getFlagIcon(*iter));
+ flags.erase(iter);
+ } else if (!compact) {
+ result.append(QString());
+ }
+ }
+
+ for (auto iter = flags.begin(); iter != flags.end(); ++iter) {
+ auto iconPath = getFlagIcon(*iter);
+ if (!iconPath.isEmpty())
+ result.append(iconPath);
+ }
+
+ return result;
+}
+
+QList<QString> ModConflictIconDelegate::getIcons(const QModelIndex &index) const
+{
+ QVariant modid = index.data(Qt::UserRole + 1);
+
+ if (modid.isValid()) {
+ ModInfo::Ptr info = ModInfo::getByIndex(modid.toInt());
+ return getIconsForFlags(info->getConflictFlags(), m_Compact);
+ }
+
+ return {};
+}
+
+QString ModConflictIconDelegate::getFlagIcon(ModInfo::EConflictFlag flag)
+{
+ switch (flag) {
+ case ModInfo::FLAG_CONFLICT_MIXED: return QStringLiteral(":/MO/gui/emblem_conflict_mixed");
+ case ModInfo::FLAG_CONFLICT_OVERWRITE: return QStringLiteral(":/MO/gui/emblem_conflict_overwrite");
+ case ModInfo::FLAG_CONFLICT_OVERWRITTEN: return QStringLiteral(":/MO/gui/emblem_conflict_overwritten");
+ case ModInfo::FLAG_CONFLICT_REDUNDANT: return QStringLiteral(":/MO/gui/emblem_conflict_redundant");
+ case ModInfo::FLAG_ARCHIVE_LOOSE_CONFLICT_OVERWRITE: return QStringLiteral(":/MO/gui/archive_loose_conflict_overwrite");
+ case ModInfo::FLAG_ARCHIVE_LOOSE_CONFLICT_OVERWRITTEN: return QStringLiteral(":/MO/gui/archive_loose_conflict_overwritten");
+ case ModInfo::FLAG_ARCHIVE_CONFLICT_MIXED: return QStringLiteral(":/MO/gui/archive_conflict_mixed");
+ case ModInfo::FLAG_ARCHIVE_CONFLICT_OVERWRITE: return QStringLiteral(":/MO/gui/archive_conflict_winner");
+ case ModInfo::FLAG_ARCHIVE_CONFLICT_OVERWRITTEN: return QStringLiteral(":/MO/gui/archive_conflict_loser");
+ case ModInfo::FLAG_OVERWRITE_CONFLICT: return QString();
+ default:
+ log::warn("ModInfo flag {} has no defined icon", flag);
+ return QString();
+ }
+}
+
+size_t ModConflictIconDelegate::getNumIcons(const QModelIndex &index) const
+{
+ unsigned int modIdx = index.data(Qt::UserRole + 1).toInt();
+ if (modIdx < ModInfo::getNumMods()) {
+ ModInfo::Ptr info = ModInfo::getByIndex(modIdx);
+ std::vector<ModInfo::EConflictFlag> flags = info->getConflictFlags();
+ size_t count = flags.size();
+ if (std::find_first_of(flags.begin(), flags.end(), m_ConflictFlags, m_ConflictFlags + 4) == flags.end()) {
+ ++count;
+ }
+ return count;
+ } else {
+ return 0;
+ }
+}
+
+
+QSize ModConflictIconDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &modelIndex) const
+{
+ size_t count = getNumIcons(modelIndex);
+ unsigned int index = modelIndex.data(Qt::UserRole + 1).toInt();
+ QSize result;
+ if (index < ModInfo::getNumMods()) {
+ result = QSize(static_cast<int>(count) * 40, 20);
+ } else {
+ result = QSize(1, 20);
+ }
+ if (option.rect.width() > 0) {
+ result.setWidth(std::min(option.rect.width(), result.width()));
+ }
+ return result;
+}
+
diff --git a/src/modconflicticondelegate.h b/src/modconflicticondelegate.h
new file mode 100644
index 00000000..8645da12
--- /dev/null
+++ b/src/modconflicticondelegate.h
@@ -0,0 +1,36 @@
+#ifndef MODCONFLICTICONDELEGATE_H
+#define MODCONFLICTICONDELEGATE_H
+
+#include "icondelegate.h"
+
+class ModConflictIconDelegate : public IconDelegate
+{
+ Q_OBJECT;
+
+public:
+ explicit ModConflictIconDelegate(QObject *parent = 0, int logicalIndex = -1, int compactSize = 80);
+ virtual QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const;
+
+ static QList<QString> getIconsForFlags(
+ std::vector<ModInfo::EConflictFlag> flags, bool compact);
+
+ static QString getFlagIcon(ModInfo::EConflictFlag flag);
+
+public slots:
+ void columnResized(int logicalIndex, int oldSize, int newSize);
+
+protected:
+ virtual QList<QString> getIcons(const QModelIndex &index) const;
+ virtual size_t getNumIcons(const QModelIndex &index) const;
+
+private:
+ static ModInfo::EConflictFlag m_ConflictFlags[4];
+ static ModInfo::EConflictFlag m_ArchiveLooseConflictFlags[2];
+ static ModInfo::EConflictFlag m_ArchiveConflictFlags[3];
+
+ int m_LogicalIndex;
+ int m_CompactSize;
+ bool m_Compact;
+};
+
+#endif // MODCONFLICTICONDELEGATE_H
diff --git a/src/modflagicondelegate.cpp b/src/modflagicondelegate.cpp
index c3142962..6e1df147 100644
--- a/src/modflagicondelegate.cpp
+++ b/src/modflagicondelegate.cpp
@@ -1,18 +1,8 @@
#include "modflagicondelegate.h"
+#include <log.h>
#include <QList>
-
-ModInfo::EFlag ModFlagIconDelegate::m_ConflictFlags[4] = { ModInfo::FLAG_CONFLICT_MIXED
- , ModInfo::FLAG_CONFLICT_OVERWRITE
- , ModInfo::FLAG_CONFLICT_OVERWRITTEN
- , ModInfo::FLAG_CONFLICT_REDUNDANT };
-
-ModInfo::EFlag ModFlagIconDelegate::m_ArchiveLooseConflictFlags[2] = { ModInfo::FLAG_ARCHIVE_LOOSE_CONFLICT_OVERWRITE
- , ModInfo::FLAG_ARCHIVE_LOOSE_CONFLICT_OVERWRITTEN };
-
-ModInfo::EFlag ModFlagIconDelegate::m_ArchiveConflictFlags[3] = { ModInfo::FLAG_ARCHIVE_CONFLICT_MIXED
- , ModInfo::FLAG_ARCHIVE_CONFLICT_OVERWRITE
- , ModInfo::FLAG_ARCHIVE_CONFLICT_OVERWRITTEN };
+using namespace MOBase;
ModFlagIconDelegate::ModFlagIconDelegate(QObject *parent, int logicalIndex, int compactSize)
: IconDelegate(parent)
@@ -29,87 +19,43 @@ void ModFlagIconDelegate::columnResized(int logicalIndex, int, int newSize)
}
}
-QList<QString> ModFlagIconDelegate::getIcons(const QModelIndex &index) const {
+QList<QString> ModFlagIconDelegate::getIconsForFlags(
+ std::vector<ModInfo::EFlag> flags, bool compact)
+{
QList<QString> result;
- QVariant modid = index.data(Qt::UserRole + 1);
- if (modid.isValid()) {
- ModInfo::Ptr info = ModInfo::getByIndex(modid.toInt());
- std::vector<ModInfo::EFlag> flags = info->getFlags();
-
- // Don't do flags for overwrite
- if (std::find(flags.begin(), flags.end(),ModInfo::FLAG_OVERWRITE) != flags.end())
- return result;
- // insert conflict icons to provide nicer alignment
- { // insert loose file conflicts first
- auto iter = std::find_first_of(flags.begin(), flags.end(),
- m_ConflictFlags, m_ConflictFlags + 4);
- if (iter != flags.end()) {
- result.append(getFlagIcon(*iter));
- flags.erase(iter);
- } else if (!m_Compact) {
- result.append(QString());
- }
- }
+ // Don't do flags for overwrite
+ if (std::find(flags.begin(), flags.end(), ModInfo::FLAG_OVERWRITE) != flags.end())
+ return result;
- { // insert loose vs archive overwrite second
- auto iter = std::find(flags.begin(), flags.end(),
- ModInfo::FLAG_ARCHIVE_LOOSE_CONFLICT_OVERWRITE);
- if (iter != flags.end()) {
- result.append(getFlagIcon(*iter));
- flags.erase(iter);
- } else if (!m_Compact) {
- result.append(QString());
- }
- }
+ for (auto iter = flags.begin(); iter != flags.end(); ++iter) {
+ auto iconPath = getFlagIcon(*iter);
+ if (!iconPath.isEmpty())
+ result.append(iconPath);
+ }
- { // insert loose vs archive overwritten third
- auto iter = std::find_first_of(flags.begin(), flags.end(),
- m_ArchiveLooseConflictFlags + 1, m_ArchiveLooseConflictFlags + 2);
- if (iter != flags.end()) {
- result.append(getFlagIcon(*iter));
- flags.erase(iter);
- } else if (!m_Compact) {
- result.append(QString());
- }
- }
+ return result;
+}
- { // insert archive conflicts last
- auto iter = std::find_first_of(flags.begin(), flags.end(),
- m_ArchiveConflictFlags, m_ArchiveConflictFlags + 3);
- if (iter != flags.end()) {
- result.append(getFlagIcon(*iter));
- flags.erase(iter);
- } else if (!m_Compact) {
- result.append(QString());
- }
- }
+QList<QString> ModFlagIconDelegate::getIcons(const QModelIndex &index) const
+{
+ QVariant modid = index.data(Qt::UserRole + 1);
- for (auto iter = flags.begin(); iter != flags.end(); ++iter) {
- auto iconPath = getFlagIcon(*iter);
- if (!iconPath.isEmpty())
- result.append(iconPath);
- }
+ if (modid.isValid()) {
+ ModInfo::Ptr info = ModInfo::getByIndex(modid.toInt());
+ return getIconsForFlags(info->getFlags(), m_Compact);
}
- return result;
+
+ return {};
}
-QString ModFlagIconDelegate::getFlagIcon(ModInfo::EFlag flag) const
+QString ModFlagIconDelegate::getFlagIcon(ModInfo::EFlag flag)
{
switch (flag) {
case ModInfo::FLAG_BACKUP: return QStringLiteral(":/MO/gui/emblem_backup");
case ModInfo::FLAG_INVALID: return QStringLiteral(":/MO/gui/problem");
case ModInfo::FLAG_NOTENDORSED: return QStringLiteral(":/MO/gui/emblem_notendorsed");
case ModInfo::FLAG_NOTES: return QStringLiteral(":/MO/gui/emblem_notes");
- case ModInfo::FLAG_CONFLICT_MIXED: return QStringLiteral(":/MO/gui/emblem_conflict_mixed");
- case ModInfo::FLAG_CONFLICT_OVERWRITE: return QStringLiteral(":/MO/gui/emblem_conflict_overwrite");
- case ModInfo::FLAG_CONFLICT_OVERWRITTEN: return QStringLiteral(":/MO/gui/emblem_conflict_overwritten");
- case ModInfo::FLAG_CONFLICT_REDUNDANT: return QStringLiteral(":/MO/gui/emblem_conflict_redundant");
- case ModInfo::FLAG_ARCHIVE_LOOSE_CONFLICT_OVERWRITE: return QStringLiteral(":/MO/gui/archive_loose_conflict_overwrite");
- case ModInfo::FLAG_ARCHIVE_LOOSE_CONFLICT_OVERWRITTEN: return QStringLiteral(":/MO/gui/archive_loose_conflict_overwritten");
- case ModInfo::FLAG_ARCHIVE_CONFLICT_MIXED: return QStringLiteral(":/MO/gui/archive_conflict_mixed");
- case ModInfo::FLAG_ARCHIVE_CONFLICT_OVERWRITE: return QStringLiteral(":/MO/gui/archive_conflict_winner");
- case ModInfo::FLAG_ARCHIVE_CONFLICT_OVERWRITTEN: return QStringLiteral(":/MO/gui/archive_conflict_loser");
case ModInfo::FLAG_ALTERNATE_GAME: return QStringLiteral(":/MO/gui/alternate_game");
case ModInfo::FLAG_FOREIGN: return QString();
case ModInfo::FLAG_SEPARATOR: return QString();
@@ -117,7 +63,7 @@ QString ModFlagIconDelegate::getFlagIcon(ModInfo::EFlag flag) const
case ModInfo::FLAG_PLUGIN_SELECTED: return QString();
case ModInfo::FLAG_TRACKED: return QStringLiteral(":/MO/gui/tracked");
default:
- qWarning("ModInfo flag %d has no defined icon", flag);
+ log::warn("ModInfo flag {} has no defined icon", flag);
return QString();
}
}
@@ -128,11 +74,7 @@ size_t ModFlagIconDelegate::getNumIcons(const QModelIndex &index) const
if (modIdx < ModInfo::getNumMods()) {
ModInfo::Ptr info = ModInfo::getByIndex(modIdx);
std::vector<ModInfo::EFlag> flags = info->getFlags();
- size_t count = flags.size();
- if (std::find_first_of(flags.begin(), flags.end(), m_ConflictFlags, m_ConflictFlags + 4) == flags.end()) {
- ++count;
- }
- return count;
+ return flags.size();
} else {
return 0;
}
diff --git a/src/modflagicondelegate.h b/src/modflagicondelegate.h
index eb6a76ab..ecab7e95 100644
--- a/src/modflagicondelegate.h
+++ b/src/modflagicondelegate.h
@@ -5,26 +5,25 @@
class ModFlagIconDelegate : public IconDelegate
{
-Q_OBJECT
+ Q_OBJECT;
public:
explicit ModFlagIconDelegate(QObject *parent = 0, int logicalIndex = -1, int compactSize = 120);
virtual QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const;
+ static QList<QString> getIconsForFlags(
+ std::vector<ModInfo::EFlag> flags, bool compact);
+
+ static QString getFlagIcon(ModInfo::EFlag flag);
+
public slots:
void columnResized(int logicalIndex, int oldSize, int newSize);
-private:
+protected:
virtual QList<QString> getIcons(const QModelIndex &index) const;
virtual size_t getNumIcons(const QModelIndex &index) const;
- QString getFlagIcon(ModInfo::EFlag flag) const;
-
private:
- static ModInfo::EFlag m_ConflictFlags[4];
- static ModInfo::EFlag m_ArchiveLooseConflictFlags[2];
- static ModInfo::EFlag m_ArchiveConflictFlags[3];
-
int m_LogicalIndex;
int m_CompactSize;
bool m_Compact;
diff --git a/src/modinfo.cpp b/src/modinfo.cpp
index 3484b644..6e048bfb 100644
--- a/src/modinfo.cpp
+++ b/src/modinfo.cpp
@@ -37,11 +37,11 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#include <appconfig.h>
#include <scriptextender.h>
#include <unmanagedmods.h>
+#include <log.h>
#include <QApplication>
#include <QDirIterator>
#include <QMutexLocker>
-#include <QSettings>
using namespace MOBase;
using namespace MOShared;
@@ -61,17 +61,32 @@ bool ModInfo::ByName(const ModInfo::Ptr &LHS, const ModInfo::Ptr &RHS)
return QString::compare(LHS->name(), RHS->name(), Qt::CaseInsensitive) < 0;
}
+bool ModInfo::isSeparatorName(const QString& name)
+{
+ static QRegExp separatorExp(".*_separator");
+ return separatorExp.exactMatch(name);
+}
+
+bool ModInfo::isBackupName(const QString& name)
+{
+ static QRegExp backupExp(".*backup[0-9]*");
+ return backupExp.exactMatch(name);
+}
+
+bool ModInfo::isRegularName(const QString& name)
+{
+ return !isSeparatorName(name) && !isBackupName(name);
+}
+
ModInfo::Ptr ModInfo::createFrom(PluginContainer *pluginContainer, const MOBase::IPluginGame *game, const QDir &dir, DirectoryEntry **directoryStructure)
{
QMutexLocker locker(&s_Mutex);
- // int id = s_NextID++;
- static QRegExp backupExp(".*backup[0-9]*");
- static QRegExp separatorExp(".*_separator");
ModInfo::Ptr result;
- if (backupExp.exactMatch(dir.dirName())) {
+
+ if (isBackupName(dir.dirName())) {
result = ModInfo::Ptr(new ModInfoBackup(pluginContainer, game, dir, directoryStructure));
- } else if(separatorExp.exactMatch(dir.dirName())){
+ } else if (isSeparatorName(dir.dirName())) {
result = Ptr(new ModInfoSeparator(pluginContainer, game, dir, directoryStructure));
} else {
result = ModInfo::Ptr(new ModInfoRegular(pluginContainer, game, dir, directoryStructure));
@@ -319,13 +334,12 @@ bool ModInfo::checkAllForUpdate(PluginContainer *pluginContainer, QObject *recei
}
if (organizedGames.empty()) {
- qWarning() << tr("All of your mods have been checked recently. We restrict update checks to help preserve your available API requests.");
+ log::warn("{}", tr("All of your mods have been checked recently. We restrict update checks to help preserve your available API requests."));
updatesAvailable = false;
} else {
- qInfo() << tr(
+ log::info("{}", tr(
"You have mods that haven't been checked within the last month using the new API. These mods must be checked before we can use the bulk update API. "
- "This will consume significantly more API requests than usual. You will need to rerun the update check once complete in order to parse the remaining mods."
- );
+ "This will consume significantly more API requests than usual. You will need to rerun the update check once complete in order to parse the remaining mods."));
}
for (auto game : organizedGames)
@@ -412,7 +426,7 @@ void ModInfo::manualUpdateCheck(PluginContainer *pluginContainer, QObject *recei
});
if (mods.size()) {
- qInfo("Checking updates for %d mods...", mods.size());
+ log::info("Checking updates for {} mods...", mods.size());
for (auto mod : mods) {
organizedGames.insert(std::make_pair<QString, int>(mod->getGameName().toLower(), mod->getNexusID()));
@@ -422,7 +436,7 @@ void ModInfo::manualUpdateCheck(PluginContainer *pluginContainer, QObject *recei
NexusInterface::instance(pluginContainer)->requestUpdates(game.second, receiver, QVariant(), game.first, QString());
}
} else {
- qInfo("None of the selected mods can be updated.");
+ log::info("None of the selected mods can be updated.");
}
}
@@ -530,10 +544,7 @@ QUrl ModInfo::parseCustomURL() const
const auto url = QUrl::fromUserInput(getCustomURL());
if (!url.isValid()) {
- qCritical()
- << "mod '" << name() << "' has an invalid custom url "
- << "'" << getCustomURL() << "'";
-
+ log::error("mod '{}' has an invalid custom url '{}'", name(), getCustomURL());
return {};
}
diff --git a/src/modinfo.h b/src/modinfo.h
index e395f45b..7c41e0a1 100644
--- a/src/modinfo.h
+++ b/src/modinfo.h
@@ -60,14 +60,7 @@ public:
static QString s_HiddenExt;
- enum EFlag {
- FLAG_INVALID,
- FLAG_BACKUP,
- FLAG_SEPARATOR,
- FLAG_OVERWRITE,
- FLAG_FOREIGN,
- FLAG_NOTENDORSED,
- FLAG_NOTES,
+ enum EConflictFlag {
FLAG_CONFLICT_OVERWRITE,
FLAG_CONFLICT_OVERWRITTEN,
FLAG_CONFLICT_MIXED,
@@ -77,6 +70,17 @@ public:
FLAG_ARCHIVE_CONFLICT_OVERWRITE,
FLAG_ARCHIVE_CONFLICT_OVERWRITTEN,
FLAG_ARCHIVE_CONFLICT_MIXED,
+ FLAG_OVERWRITE_CONFLICT,
+ };
+
+ enum EFlag {
+ FLAG_INVALID,
+ FLAG_BACKUP,
+ FLAG_SEPARATOR,
+ FLAG_OVERWRITE,
+ FLAG_FOREIGN,
+ FLAG_NOTENDORSED,
+ FLAG_NOTES,
FLAG_PLUGIN_SELECTED,
FLAG_ALTERNATE_GAME,
FLAG_TRACKED,
@@ -229,6 +233,18 @@ public:
*/
static ModInfo::Ptr createFromPlugin(const QString &modName, const QString &espName, const QStringList &bsaNames, ModInfo::EModType modType, MOShared::DirectoryEntry **directoryStructure, PluginContainer *pluginContainer);
+ // whether the given name is used for separators
+ //
+ static bool isSeparatorName(const QString& name);
+
+ // whether the given name is used for backups
+ //
+ static bool isBackupName(const QString& name);
+
+ // whether the given name is used for regular mods
+ //
+ static bool isRegularName(const QString& name);
+
/**
* @brief retieve a name for one of the CONTENT_ enums
* @param contentType the content value
@@ -508,6 +524,11 @@ public:
virtual std::vector<EFlag> getFlags() const = 0;
/**
+ * @return a list of conflict flags for this mod
+ */
+ virtual std::vector<EConflictFlag> getConflictFlags() const = 0;
+
+ /**
* @return a list of content types contained in a mod
*/
virtual std::vector<EContent> getContents() const { return std::vector<EContent>(); }
diff --git a/src/modinfobackup.cpp b/src/modinfobackup.cpp
index 6e307103..6a34b86a 100644
--- a/src/modinfobackup.cpp
+++ b/src/modinfobackup.cpp
@@ -1,5 +1,6 @@
#include "modinfobackup.h"
+
std::vector<ModInfo::EFlag> ModInfoBackup::getFlags() const
{
std::vector<ModInfo::EFlag> result = ModInfoRegular::getFlags();
diff --git a/src/modinfobackup.h b/src/modinfobackup.h
index cab613c9..393c2e38 100644
--- a/src/modinfobackup.h
+++ b/src/modinfobackup.h
@@ -6,6 +6,8 @@
class ModInfoBackup : public ModInfoRegular
{
+ Q_OBJECT
+
friend class ModInfo;
public:
diff --git a/src/modinfodialog.cpp b/src/modinfodialog.cpp
index 47ac84be..f5ca1de7 100644
--- a/src/modinfodialog.cpp
+++ b/src/modinfodialog.cpp
@@ -61,10 +61,27 @@ bool canPreviewFile(
return pluginContainer.previewGenerator().previewSupported(ext);
}
-bool canOpenFile(bool isArchive, const QString&)
+bool isExecutableFilename(const QString& filename)
{
- // can open anything as long as it's not in an archive
- return !isArchive;
+ static const std::set<QString> exeExtensions = {
+ "exe", "cmd", "bat"
+ };
+
+ const auto ext = QFileInfo(filename).suffix().toLower();
+
+ return exeExtensions.contains(ext);
+}
+
+bool canRunFile(bool isArchive, const QString& filename)
+{
+ // can run executables that are not archives
+ return !isArchive && isExecutableFilename(filename);
+}
+
+bool canOpenFile(bool isArchive, const QString& filename)
+{
+ // can open non-executables that are not archives
+ return !isArchive && !isExecutableFilename(filename);
}
bool canExploreFile(bool isArchive, const QString&)
@@ -176,7 +193,7 @@ void ModInfoDialog::createTabs()
// check for tabs in the ui not having a corresponding tab in the list
int count = ui->tabWidget->count();
if (count < 0 || count > static_cast<int>(m_tabs.size())) {
- qCritical() << "mod info dialog has more tabs than expected";
+ log::error("mod info dialog has more tabs than expected");
count = static_cast<int>(m_tabs.size());
}
@@ -210,6 +227,9 @@ void ModInfoDialog::createTabs()
int ModInfoDialog::exec()
{
+ GeometrySaver gs(Settings::instance(), this);
+ restoreState();
+
// whether to select the first tab; if the main window requested a specific
// tab, it is selected when encountered in update()
const auto selectFirst = (m_initialTab == ModInfoTabIDs::None);
@@ -220,7 +240,10 @@ int ModInfoDialog::exec()
ui->tabWidget->setCurrentIndex(0);
}
- return TutorableDialog::exec();
+ const int r = TutorableDialog::exec();
+ saveState();
+
+ return r;
}
void ModInfoDialog::setMod(ModInfo::Ptr mod)
@@ -239,13 +262,13 @@ void ModInfoDialog::setMod(const QString& name)
{
unsigned int index = ModInfo::getIndex(name);
if (index == UINT_MAX) {
- qCritical() << "failed to resolve mod name " << name;
+ log::error("failed to resolve mod name {}", name);
return;
}
auto mod = ModInfo::getByIndex(index);
if (!mod) {
- qCritical() << "mod by index " << index << " is null";
+ log::error("mod by index {} is null", index);
return;
}
@@ -307,7 +330,7 @@ void ModInfoDialog::update(bool firstTime)
// changed
tabInfo->tab->activated();
} else {
- qCritical() << "tab index " << oldTab << " not found";
+ log::error("tab index {} not found", oldTab);
}
}
}
@@ -356,7 +379,7 @@ void ModInfoDialog::setTabsVisibility(bool firstTime)
if (!firstTime) {
// but don't do it the first time visibility is set because the tabs are
// in the default order, which will clobber the current settings
- saveTabOrder(Settings::instance());
+ saveTabOrder();
}
// remember selection, if any
@@ -375,11 +398,14 @@ void ModInfoDialog::reAddTabs(
Q_ASSERT(visibility.size() == m_tabs.size());
// ordered tab names from settings
- const auto orderedNames = getOrderedTabNames();
+ const auto orderedNames = m_core->settings().geometry().modInfoTabOrder();
- // whether the tabs can be sorted; if the object name of a tab widget is not
- // found in orderedNames, the list cannot be sorted safely
- bool canSort = true;
+ // whether the tabs can be sorted
+ //
+ // if the object name of a tab widget is not found in orderedNames, the list
+ // cannot be sorted safely; if the list is empty, it's probably a first run
+ // and there's nothing to sort
+ bool canSort = !orderedNames.empty();
// gathering visible tabs
std::vector<TabInfo*> visibleTabs;
@@ -400,7 +426,7 @@ void ModInfoDialog::reAddTabs(
if (itor == orderedNames.end()) {
// this shouldn't happen, it means there's a tab in the UI that's no
// in the list
- qCritical() << "can't sort tabs, '" << objectName << "' not found";
+ log::error("can't sort tabs, '{}' not found", objectName);
canSort = false;
}
}
@@ -556,9 +582,7 @@ void ModInfoDialog::switchToTab(ModInfoTabIDs id)
}
// this could happen if the tab is not visible right now
- qDebug()
- << "can't switch to tab ID " << static_cast<int>(id)
- << ", not available";
+ log::debug("can't switch to tab ID {}, not available", static_cast<int>(id));
}
MOShared::FilesOrigin* ModInfoDialog::getOrigin()
@@ -577,37 +601,28 @@ MOShared::FilesOrigin* ModInfoDialog::getOrigin()
return origin;
}
-void ModInfoDialog::saveState(Settings& s) const
+void ModInfoDialog::saveState() const
{
- saveTabOrder(s);
-
- // remove 2.2.0 settings
- s.directInterface().remove("mod_info_tabs");
- s.directInterface().remove("mod_info_conflict_expanders");
- s.directInterface().remove("mod_info_conflicts");
- s.directInterface().remove("mod_info_advanced_conflicts");
- s.directInterface().remove("mod_info_conflicts_overwrite");
- s.directInterface().remove("mod_info_conflicts_noconflict");
- s.directInterface().remove("mod_info_conflicts_overwritten");
+ saveTabOrder();
// save state for each tab
for (const auto& tabInfo : m_tabs) {
- tabInfo.tab->saveState(s);
+ tabInfo.tab->saveState(m_core->settings());
}
}
-void ModInfoDialog::restoreState(const Settings& s)
+void ModInfoDialog::restoreState()
{
// tab order is not restored here, it will be picked up if tabs have to be
// removed and re-added
// restore state for each tab
for (const auto& tabInfo : m_tabs) {
- tabInfo.tab->restoreState(s);
+ tabInfo.tab->restoreState(m_core->settings());
}
}
-void ModInfoDialog::saveTabOrder(Settings& s) const
+void ModInfoDialog::saveTabOrder() const
{
if (static_cast<int>(m_tabs.size()) != ui->tabWidget->count()) {
// only save tab state when all tabs are visible
@@ -631,40 +646,7 @@ void ModInfoDialog::saveTabOrder(Settings& s) const
names += ui->tabWidget->widget(i)->objectName();
}
- s.directInterface().setValue("mod_info_tab_order", names);
-}
-
-std::vector<QString> ModInfoDialog::getOrderedTabNames() const
-{
- const auto& settings = Settings::instance().directInterface();
-
- std::vector<QString> v;
-
- if (settings.contains("mod_info_tabs")) {
- // old byte array from 2.2.0
- QDataStream stream(settings.value("mod_info_tabs").toByteArray());
-
- int count = 0;
- stream >> count;
-
- for (int i=0; i<count; ++i) {
- QString s;
- stream >> s;
- v.emplace_back(std::move(s));
- }
- } else {
- // string list
- QString string = settings.value("mod_info_tab_order").toString();
- QTextStream stream(&string);
-
- while (!stream.atEnd()) {
- QString s;
- stream >> s;
- v.emplace_back(std::move(s));
- }
- }
-
- return v;
+ m_core->settings().geometry().setModInfoTabOrder(names);
}
void ModInfoDialog::onOriginModified(int originID)
@@ -753,7 +735,7 @@ void ModInfoDialog::onTabMoved()
}
if (!found) {
- qCritical() << "unknown tab at index " << i;
+ log::error("unknown tab at index {}", i);
}
}
}
diff --git a/src/modinfodialog.h b/src/modinfodialog.h
index 34555b0c..48680ca4 100644
--- a/src/modinfodialog.h
+++ b/src/modinfodialog.h
@@ -61,18 +61,11 @@ public:
//
void selectTab(ModInfoTabIDs id);
- // updates all tabs, selects the initial tab and opens the dialog
+ // updates all tabs, selects the initial tab, opens the dialog and
+ // saves/restores geometry
//
int exec() override;
- // saves the dialog state and calls saveState() on all tabs
- //
- void saveState(Settings& s) const;
-
- // restores the dialog state and calls restoreState() on all tabs
- //
- void restoreState(const Settings& s);
-
signals:
// emitted when a tab changes the origin
//
@@ -146,6 +139,15 @@ private:
void createTabs();
+ // saves the dialog state and calls saveState() on all tabs
+ //
+ void saveState() const;
+
+ // restores the dialog state and calls restoreState() on all tabs
+ //
+ void restoreState();
+
+
// sets the currently selected mod; resets first activation, but doesn't
// update anything
//
@@ -213,11 +215,7 @@ private:
// setTabsVisibility() to make sure any changes to order are saved before
// re-adding tabs
//
- void saveTabOrder(Settings& s) const;
-
- // returns a list of tab names in the order they should appear on the widget
- //
- std::vector<QString> getOrderedTabNames() const;
+ void saveTabOrder() const;
// asks all the tabs if they accept closing the dialog, returns false if one
// objected
diff --git a/src/modinfodialog.ui b/src/modinfodialog.ui
index 5b559992..d7b03d70 100644
--- a/src/modinfodialog.ui
+++ b/src/modinfodialog.ui
@@ -268,6 +268,13 @@
<number>0</number>
</property>
<item>
+ <widget class="QToolButton" name="previewPluginButton">
+ <property name="text">
+ <string>Open with Preview Plugin</string>
+ </property>
+ </widget>
+ </item>
+ <item>
<widget class="QToolButton" name="imagesExplore">
<property name="text">
<string>Open in Explorer</string>
@@ -384,7 +391,7 @@ Most mods do not have optional esps, so chances are good you are looking at an e
<string>Move a file to the data directory.</string>
</property>
<property name="whatsThis">
- <string>This moves a esp to the esp directory so it can be enabled in the main window. Please note that the ESP merely becomes &quot;available&quot;, it will not necessarily be loaded! That is configured in the main window of omo.</string>
+ <string>This moves a esp to the esp directory so it can be enabled in the main window. Please note that the ESP merely becomes &quot;available&quot;, it will not necessarily be loaded! That is configured in the main window of MO.</string>
</property>
<property name="text">
<string/>
@@ -404,10 +411,10 @@ Most mods do not have optional esps, so chances are good you are looking at an e
<item>
<widget class="QToolButton" name="deactivateESP">
<property name="toolTip">
- <string>Make the selected mod in the lower list unavailable.</string>
+ <string>Make the selected mod in the right list unavailable.</string>
</property>
<property name="whatsThis">
- <string>The selected esp (in the lower list) will be pushed into a subdirectory of the mod and will thus become &quot;invisible&quot; to the game. It can then no longer be activated.</string>
+ <string>The selected esp (in the right list) will be pushed into a subdirectory of the mod and will thus become &quot;invisible&quot; to the game. It can then no longer be activated.</string>
</property>
<property name="text">
<string/>
@@ -1251,6 +1258,9 @@ p, li { white-space: pre-wrap; }
<property name="uniformRowHeights">
<bool>true</bool>
</property>
+ <property name="sortingEnabled">
+ <bool>true</bool>
+ </property>
</widget>
</item>
</layout>
diff --git a/src/modinfodialogconflicts.cpp b/src/modinfodialogconflicts.cpp
index 511d48ad..0fb8c5a6 100644
--- a/src/modinfodialogconflicts.cpp
+++ b/src/modinfodialogconflicts.cpp
@@ -7,13 +7,11 @@
using namespace MOShared;
using namespace MOBase;
-namespace shell = MOBase::shell;
// if there are more than 50 selected items in the conflict tree, don't bother
// checking whether menu items apply to them, just show all of them
const std::size_t max_small_selection = 50;
-
class ConflictItem
{
public:
@@ -82,6 +80,11 @@ public:
return canUnhideFile(isArchive(), fileName());
}
+ bool canRun() const
+ {
+ return canRunFile(isArchive(), fileName());
+ }
+
bool canOpen() const
{
return canOpenFile(isArchive(), fileName());
@@ -365,7 +368,7 @@ void for_each_in_selection(QTreeView* tree, F&& f)
const auto* model = dynamic_cast<ConflictListModel*>(tree->model());
if (!model) {
- qCritical() << "tree doesn't have a ConflictListModel";
+ log::error("tree doesn't have a ConflictListModel");
return;
}
@@ -410,8 +413,7 @@ void ConflictsTab::clear()
void ConflictsTab::saveState(Settings& s)
{
- s.directInterface().setValue(
- "mod_info_conflicts_tab", ui->tabConflictsTabs->currentIndex());
+ s.widgets().saveIndex(ui->tabConflictsTabs);
m_general.saveState(s);
m_advanced.saveState(s);
@@ -419,8 +421,7 @@ void ConflictsTab::saveState(Settings& s)
void ConflictsTab::restoreState(const Settings& s)
{
- ui->tabConflictsTabs->setCurrentIndex(
- s.directInterface().value("mod_info_conflicts_tab", 0).toInt());
+ s.widgets().restoreIndex(ui->tabConflictsTabs, 0);
m_general.restoreState(s);
m_advanced.restoreState(s);
@@ -438,10 +439,18 @@ void ConflictsTab::changeItemsVisibility(QTreeView* tree, bool visible)
const auto n = smallSelectionSize(tree);
- qDebug().nospace().noquote()
- << (visible ? "unhiding" : "hiding") << " "
- << (n > max_small_selection ? "a lot of" : QString("%1").arg(n))
- << " conflict files";
+ // logging
+ {
+ const QString action = (visible ? "unhiding" : "hiding");
+
+ QString files;
+ if (n > max_small_selection)
+ files = "a lot of";
+ else
+ files = QString("%1").arg(n);
+
+ log::debug("{} {} conflict files", action, files);
+ }
QFlags<FileRenamer::RenameFlags> flags =
(visible ? FileRenamer::UNHIDE : FileRenamer::HIDE);
@@ -454,7 +463,7 @@ void ConflictsTab::changeItemsVisibility(QTreeView* tree, bool visible)
auto* model = dynamic_cast<ConflictListModel*>(tree->model());
if (!model) {
- qCritical() << "list doesn't have a ConflictListModel";
+ log::error("list doesn't have a ConflictListModel");
return;
}
@@ -467,7 +476,7 @@ void ConflictsTab::changeItemsVisibility(QTreeView* tree, bool visible)
if (visible) {
if (!item->canUnhide()) {
- qDebug().nospace() << "cannot unhide " << item->relativeName() << ", skipping";
+ log::debug("cannot unhide {}, skipping", item->relativeName());
return true;
}
@@ -475,7 +484,7 @@ void ConflictsTab::changeItemsVisibility(QTreeView* tree, bool visible)
} else {
if (!item->canHide()) {
- qDebug().nospace() << "cannot hide " << item->relativeName() << ", skipping";
+ log::debug("cannot hide {}, skipping", item->relativeName());
return true;
}
@@ -504,10 +513,10 @@ void ConflictsTab::changeItemsVisibility(QTreeView* tree, bool visible)
return true;
});
- qDebug().nospace() << (visible ? "unhiding" : "hiding") << " conflict files done";
+ log::debug("{} conflict files done", (visible ? "unhiding" : "hiding"));
if (changed) {
- qDebug().nospace() << "triggering refresh";
+ log::debug("triggering refresh");
if (origin()) {
emitOriginModified();
@@ -517,32 +526,65 @@ void ConflictsTab::changeItemsVisibility(QTreeView* tree, bool visible)
}
}
-void ConflictsTab::openItems(QTreeView* tree)
+void ConflictsTab::activateItems(QTreeView* tree)
{
+ const auto tryPreview = core().settings().interface().doubleClicksOpenPreviews();
+
// the menu item is only shown for a single selection, but handle all of them
// in case this changes
for_each_in_selection(tree, [&](const ConflictItem* item) {
- core().executeFileVirtualized(parentWidget(), item->fileName());
+ const auto path = item->fileName();
+
+ if (tryPreview && canPreviewFile(plugin(), item->isArchive(), path)) {
+ previewItem(item);
+ } else {
+ openItem(item, false);
+ }
+
return true;
});
}
+void ConflictsTab::openItems(QTreeView* tree, bool hooked)
+{
+ // the menu item is only shown for a single selection, but handle all of them
+ // in case this changes
+ for_each_in_selection(tree, [&](const ConflictItem* item) {
+ openItem(item, hooked);
+ return true;
+ });
+}
+
+void ConflictsTab::openItem(const ConflictItem* item, bool hooked)
+{
+ core().processRunner()
+ .setFromFile(parentWidget(), item->fileName())
+ .setHooked(hooked)
+ .setWaitForCompletion()
+ .run();
+}
+
void ConflictsTab::previewItems(QTreeView* tree)
{
// the menu item is only shown for a single selection, but handle all of them
// in case this changes
for_each_in_selection(tree, [&](const ConflictItem* item) {
- core().previewFileWithAlternatives(parentWidget(), item->fileName());
+ previewItem(item);
return true;
});
}
+void ConflictsTab::previewItem(const ConflictItem* item)
+{
+ core().previewFileWithAlternatives(parentWidget(), item->fileName());
+}
+
void ConflictsTab::exploreItems(QTreeView* tree)
{
// the menu item is only shown for a single selection, but handle all of them
// in case this changes
for_each_in_selection(tree, [&](const ConflictItem* item) {
- shell::ExploreFile(item->fileName());
+ shell::Explore(item->fileName());
return true;
});
}
@@ -556,10 +598,8 @@ void ConflictsTab::showContextMenu(const QPoint &pos, QTreeView* tree)
// open
if (actions.open) {
connect(actions.open, &QAction::triggered, [&]{
- openItems(tree);
+ openItems(tree, false);
});
-
- menu.addAction(actions.open);
}
// preview
@@ -567,8 +607,46 @@ void ConflictsTab::showContextMenu(const QPoint &pos, QTreeView* tree)
connect(actions.preview, &QAction::triggered, [&]{
previewItems(tree);
});
+ }
+
+ if ((actions.open && actions.open->isEnabled()) && (actions.preview && actions.preview->isEnabled())) {
+ if (Settings::instance().interface().doubleClicksOpenPreviews()) {
+ menu.addAction(actions.preview);
+ menu.addAction(actions.open);
+ } else {
+ menu.addAction(actions.open);
+ menu.addAction(actions.preview);
+ }
+ } else {
+ if (actions.open) {
+ menu.addAction(actions.open);
+ }
+
+ if (actions.preview) {
+ menu.addAction(actions.preview);
+ }
+ }
+
+ // run hooked
+ if (actions.runHooked) {
+ connect(actions.runHooked, &QAction::triggered, [&]{
+ openItems(tree, true);
+ });
- menu.addAction(actions.preview);
+ menu.addAction(actions.runHooked);
+ }
+
+ // goto
+ if (actions.gotoMenu) {
+ menu.addMenu(actions.gotoMenu);
+
+ for (auto* a : actions.gotoActions) {
+ connect(a, &QAction::triggered, [&, name=a->text()]{
+ emitModOpen(name);
+ });
+
+ actions.gotoMenu->addAction(a);
+ }
}
// explore
@@ -580,6 +658,8 @@ void ConflictsTab::showContextMenu(const QPoint &pos, QTreeView* tree)
menu.addAction(actions.explore);
}
+ menu.addSeparator();
+
// hide
if (actions.hide) {
connect(actions.hide, &QAction::triggered, [&]{
@@ -598,20 +678,15 @@ void ConflictsTab::showContextMenu(const QPoint &pos, QTreeView* tree)
menu.addAction(actions.unhide);
}
- // goto
- if (actions.gotoMenu) {
- menu.addMenu(actions.gotoMenu);
-
- for (auto* a : actions.gotoActions) {
- connect(a, &QAction::triggered, [&, name=a->text()]{
- emitModOpen(name);
- });
-
- actions.gotoMenu->addAction(a);
+ if (!menu.isEmpty()) {
+ if (actions.open || actions.preview || actions.runHooked) {
+ // bold the first option
+ auto* top = menu.actions()[0];
+ auto f = top->font();
+ f.setBold(true);
+ top->setFont(f);
}
- }
- if (!menu.isEmpty()) {
menu.exec(tree->viewport()->mapToGlobal(pos));
}
}
@@ -624,6 +699,7 @@ ConflictsTab::Actions ConflictsTab::createMenuActions(QTreeView* tree)
bool enableHide = true;
bool enableUnhide = true;
+ bool enableRun = true;
bool enableOpen = true;
bool enablePreview = true;
bool enableExplore = true;
@@ -633,7 +709,7 @@ ConflictsTab::Actions ConflictsTab::createMenuActions(QTreeView* tree)
const auto* model = dynamic_cast<ConflictListModel*>(tree->model());
if (!model) {
- qCritical() << "tree doesn't have a ConflictListModel";
+ log::error("tree doesn't have a ConflictListModel");
return {};
}
@@ -648,6 +724,7 @@ ConflictsTab::Actions ConflictsTab::createMenuActions(QTreeView* tree)
enableHide = item->canHide();
enableUnhide = item->canUnhide();
+ enableRun = item->canRun();
enableOpen = item->canOpen();
enablePreview = item->canPreview(plugin());
enableExplore = item->canExplore();
@@ -656,6 +733,7 @@ ConflictsTab::Actions ConflictsTab::createMenuActions(QTreeView* tree)
else {
// this is a multiple selection, don't show open/preview so users don't open
// a thousand files
+ enableRun = false;
enableOpen = false;
enablePreview = false;
@@ -692,25 +770,30 @@ ConflictsTab::Actions ConflictsTab::createMenuActions(QTreeView* tree)
Actions actions;
- actions.hide = new QAction(tr("&Hide"), parentWidget());
- actions.hide->setEnabled(enableHide);
-
- // note that it is possible for hidden files to appear if they override other
- // hidden files from another mod
- actions.unhide = new QAction(tr("&Unhide"), parentWidget());
- actions.unhide->setEnabled(enableUnhide);
-
- actions.open = new QAction(tr("&Open/Execute"), parentWidget());
- actions.open->setEnabled(enableOpen);
+ if (enableRun) {
+ actions.open = new QAction(tr("&Execute"), parentWidget());
+ actions.runHooked = new QAction(tr("Execute with &VFS"), parentWidget());
+ } else if (enableOpen) {
+ actions.open = new QAction(tr("&Open"), parentWidget());
+ actions.runHooked = new QAction(tr("Open with &VFS"), parentWidget());
+ }
actions.preview = new QAction(tr("&Preview"), parentWidget());
actions.preview->setEnabled(enablePreview);
+ actions.gotoMenu = new QMenu(tr("&Go to..."), parentWidget());
+ actions.gotoMenu->setEnabled(enableGoto);
+
actions.explore = new QAction(tr("Open in &Explorer"), parentWidget());
actions.explore->setEnabled(enableExplore);
- actions.gotoMenu = new QMenu(tr("&Go to..."), parentWidget());
- actions.gotoMenu->setEnabled(enableGoto);
+ actions.hide = new QAction(tr("&Hide"), parentWidget());
+ actions.hide->setEnabled(enableHide);
+
+ // note that it is possible for hidden files to appear if they override other
+ // hidden files from another mod
+ actions.unhide = new QAction(tr("&Unhide"), parentWidget());
+ actions.unhide->setEnabled(enableUnhide);
if (enableGoto && n == 1) {
const auto* item = model->getItem(static_cast<std::size_t>(
@@ -778,11 +861,15 @@ GeneralConflictsTab::GeneralConflictsTab(
QObject::connect(
ui->overwriteTree, &QTreeView::doubleClicked,
- [&](auto&& item){ onOverwriteActivated(item); });
+ [&](auto&&){ m_tab->activateItems(ui->overwriteTree); });
QObject::connect(
ui->overwrittenTree, &QTreeView::doubleClicked,
- [&](auto&& item){ onOverwrittenActivated(item); });
+ [&](auto&& item){ m_tab->activateItems(ui->overwrittenTree); });
+
+ QObject::connect(
+ ui->noConflictTree, &QTreeView::doubleClicked,
+ [&](auto&& item){ m_tab->activateItems(ui->noConflictTree); });
QObject::connect(
ui->overwriteTree, &QTreeView::customContextMenuRequested,
@@ -810,55 +897,22 @@ void GeneralConflictsTab::clear()
void GeneralConflictsTab::saveState(Settings& s)
{
- QByteArray result;
- QDataStream stream(&result, QIODevice::WriteOnly);
-
- stream
- << m_expanders.overwrite.opened()
- << m_expanders.overwritten.opened()
- << m_expanders.nonconflict.opened();
-
- s.directInterface().setValue(
- "mod_info_conflicts_general_expanders", result);
-
- s.directInterface().setValue(
- "mod_info_conflicts_general_overwrite",
- ui->overwriteTree->header()->saveState());
-
- s.directInterface().setValue(
- "mod_info_conflicts_general_noconflict",
- ui->noConflictTree->header()->saveState());
-
- s.directInterface().setValue(
- "mod_info_conflicts_general_overwritten",
- ui->overwrittenTree->header()->saveState());
+ s.geometry().saveState(&m_expanders.overwrite);
+ s.geometry().saveState(&m_expanders.overwritten);
+ s.geometry().saveState(&m_expanders.nonconflict);
+ s.geometry().saveState(ui->overwriteTree->header());
+ s.geometry().saveState(ui->noConflictTree->header());
+ s.geometry().saveState(ui->overwrittenTree->header());
}
void GeneralConflictsTab::restoreState(const Settings& s)
{
- QDataStream stream(s.directInterface()
- .value("mod_info_conflicts_general_expanders").toByteArray());
-
- bool overwriteExpanded = false;
- bool overwrittenExpanded = false;
- bool noConflictExpanded = false;
-
- stream >> overwriteExpanded >> overwrittenExpanded >> noConflictExpanded;
-
- if (stream.status() == QDataStream::Ok) {
- m_expanders.overwrite.toggle(overwriteExpanded);
- m_expanders.overwritten.toggle(overwrittenExpanded);
- m_expanders.nonconflict.toggle(noConflictExpanded);
- }
-
- ui->overwriteTree->header()->restoreState(s.directInterface()
- .value("mod_info_conflicts_general_overwrite").toByteArray());
-
- ui->noConflictTree->header()->restoreState(s.directInterface()
- .value("mod_info_conflicts_general_noconflict").toByteArray());
-
- ui->overwrittenTree->header()->restoreState(s.directInterface()
- .value("mod_info_conflicts_general_overwritten").toByteArray());
+ s.geometry().restoreState(&m_expanders.overwrite);
+ s.geometry().restoreState(&m_expanders.overwritten);
+ s.geometry().restoreState(&m_expanders.nonconflict);
+ s.geometry().restoreState(ui->overwriteTree->header());
+ s.geometry().restoreState(ui->noConflictTree->header());
+ s.geometry().restoreState(ui->overwrittenTree->header());
}
bool GeneralConflictsTab::update()
@@ -935,7 +989,7 @@ ConflictItem GeneralConflictsTab::createOverwriteItem(
auto origin = ToQString(ds.getOriginByID(alternatives.back().first).getName());
return ConflictItem(
- ToQString(altString), std::move(relativeName), QString::null, index,
+ ToQString(altString), std::move(relativeName), QString(), index,
std::move(fileName), true, std::move(origin), archive);
}
@@ -943,8 +997,8 @@ ConflictItem GeneralConflictsTab::createNoConflictItem(
FileEntry::Index index, bool archive, QString fileName, QString relativeName)
{
return ConflictItem(
- QString::null, std::move(relativeName), QString::null, index,
- std::move(fileName), false, QString::null, archive);
+ QString(), std::move(relativeName), QString(), index,
+ std::move(fileName), false, QString(), archive);
}
ConflictItem GeneralConflictsTab::createOverwrittenItem(
@@ -958,7 +1012,7 @@ ConflictItem GeneralConflictsTab::createOverwrittenItem(
QString altOrigin = after;
return ConflictItem(
- QString::null, std::move(relativeName), std::move(after),
+ QString(), std::move(relativeName), std::move(after),
index, std::move(fileName), true, std::move(altOrigin), archive);
}
@@ -1027,6 +1081,10 @@ AdvancedConflictsTab::AdvancedConflictsTab(
[&]{ update(); });
QObject::connect(
+ ui->conflictsAdvancedList, &QTreeView::activated,
+ [&]{ m_tab->activateItems(ui->conflictsAdvancedList); });
+
+ QObject::connect(
ui->conflictsAdvancedList, &QTreeView::customContextMenuRequested,
[&](const QPoint& p){ m_tab->showContextMenu(p, ui->conflictsAdvancedList); });
@@ -1041,41 +1099,18 @@ void AdvancedConflictsTab::clear()
void AdvancedConflictsTab::saveState(Settings& s)
{
- s.directInterface().setValue(
- "mod_info_conflicts_advanced_list",
- ui->conflictsAdvancedList->header()->saveState());
-
- QByteArray result;
- QDataStream stream(&result, QIODevice::WriteOnly);
-
- stream
- << ui->conflictsAdvancedShowNoConflict->isChecked()
- << ui->conflictsAdvancedShowAll->isChecked()
- << ui->conflictsAdvancedShowNearest->isChecked();
-
- s.directInterface().setValue(
- "mod_info_conflicts_advanced_options", result);
+ s.geometry().saveState(ui->conflictsAdvancedList->header());
+ s.widgets().saveChecked(ui->conflictsAdvancedShowNoConflict);
+ s.widgets().saveChecked(ui->conflictsAdvancedShowAll);
+ s.widgets().saveChecked(ui->conflictsAdvancedShowNearest);
}
void AdvancedConflictsTab::restoreState(const Settings& s)
{
- ui->conflictsAdvancedList->header()->restoreState(
- s.directInterface().value("mod_info_conflicts_advanced_list").toByteArray());
-
- QDataStream stream(s.directInterface()
- .value("mod_info_conflicts_advanced_options").toByteArray());
-
- bool noConflictChecked = false;
- bool showAllChecked = false;
- bool showNearestChecked = false;
-
- stream >> noConflictChecked >> showAllChecked >> showNearestChecked;
-
- if (stream.status() == QDataStream::Ok) {
- ui->conflictsAdvancedShowNoConflict->setChecked(noConflictChecked);
- ui->conflictsAdvancedShowAll->setChecked(showAllChecked);
- ui->conflictsAdvancedShowNearest->setChecked(showNearestChecked);
- }
+ s.geometry().restoreState(ui->conflictsAdvancedList->header());
+ s.widgets().restoreChecked(ui->conflictsAdvancedShowNoConflict);
+ s.widgets().restoreChecked(ui->conflictsAdvancedShowAll);
+ s.widgets().restoreChecked(ui->conflictsAdvancedShowNearest);
}
void AdvancedConflictsTab::update()
@@ -1225,5 +1260,5 @@ std::optional<ConflictItem> AdvancedConflictsTab::createItem(
return ConflictItem(
std::move(beforeQS), std::move(relativeName), std::move(afterQS),
- index, std::move(fileName), hasAlts, QString::null, archive);
+ index, std::move(fileName), hasAlts, QString(), archive);
}
diff --git a/src/modinfodialogconflicts.h b/src/modinfodialogconflicts.h
index a77c2ac9..1297e536 100644
--- a/src/modinfodialogconflicts.h
+++ b/src/modinfodialogconflicts.h
@@ -33,7 +33,7 @@ signals:
private:
struct Expanders
{
- ExpanderWidget overwrite, overwritten, nonconflict;
+ MOBase::ExpanderWidget overwrite, overwritten, nonconflict;
};
ConflictsTab* m_tab;
@@ -60,10 +60,6 @@ private:
void onOverwriteActivated(const QModelIndex& index);
void onOverwrittenActivated(const QModelIndex& index);
-
- void onOverwriteTreeContext(const QPoint &pos);
- void onOverwrittenTreeContext(const QPoint &pos);
- void onNoConflictTreeContext(const QPoint &pos);
};
@@ -111,10 +107,13 @@ public:
void restoreState(const Settings& s) override;
bool canHandleUnmanaged() const override;
- void openItems(QTreeView* tree);
+ void activateItems(QTreeView* tree);
+ void openItems(QTreeView* tree, bool hooked);
void previewItems(QTreeView* tree);
void exploreItems(QTreeView* tree);
+ void openItem(const ConflictItem* item, bool hooked);
+ void previewItem(const ConflictItem* item);
void changeItemsVisibility(QTreeView* tree, bool visible);
void showContextMenu(const QPoint &pos, QTreeView* tree);
@@ -125,6 +124,7 @@ private:
QAction* hide = nullptr;
QAction* unhide = nullptr;
QAction* open = nullptr;
+ QAction* runHooked = nullptr;
QAction* preview = nullptr;
QAction* explore = nullptr;
QMenu* gotoMenu = nullptr;
diff --git a/src/modinfodialogesps.cpp b/src/modinfodialogesps.cpp
index fba5d39a..e5f1637f 100644
--- a/src/modinfodialogesps.cpp
+++ b/src/modinfodialogesps.cpp
@@ -3,8 +3,9 @@
#include "modinfodialog.h"
#include "settings.h"
#include <report.h>
+#include <log.h>
-using MOBase::reportError;
+using namespace MOBase;
class ESPItem
{
@@ -276,12 +277,12 @@ void ESPsTab::update()
void ESPsTab::saveState(Settings& s)
{
- saveWidgetState(s.directInterface(), ui->ESPsSplitter);
+ s.geometry().saveState(ui->ESPsSplitter);
}
void ESPsTab::restoreState(const Settings& s)
{
- restoreWidgetState(s.directInterface(), ui->ESPsSplitter);
+ s.geometry().restoreState(ui->ESPsSplitter);
}
void ESPsTab::onActivate()
@@ -297,7 +298,7 @@ void ESPsTab::onActivate()
}
if (esp->isActive()) {
- qWarning("ESPsTab::onActive(): item is already active");
+ log::warn("ESPsTab::onActive(): item is already active");
return;
}
@@ -348,7 +349,7 @@ void ESPsTab::onDeactivate()
}
if (!esp->isActive()) {
- qWarning("ESPsTab::onDeactivate(): item is already inactive");
+ log::warn("ESPsTab::onDeactivate(): item is already inactive");
return;
}
diff --git a/src/modinfodialogfiletree.cpp b/src/modinfodialogfiletree.cpp
index 0b519932..cc0e6493 100644
--- a/src/modinfodialogfiletree.cpp
+++ b/src/modinfodialogfiletree.cpp
@@ -5,8 +5,9 @@
#include "filerenamer.h"
#include <utility.h>
#include <report.h>
+#include <log.h>
-using MOBase::reportError;
+using namespace MOBase;
namespace shell = MOBase::shell;
// if there are more than 50 selected items in the filetree, don't bother
@@ -23,6 +24,7 @@ FileTreeTab::FileTreeTab(ModInfoDialogTabContext cx)
m_actions.newFolder = new QAction(tr("&New Folder"), ui->filetree);
m_actions.open = new QAction(tr("&Open/Execute"), ui->filetree);
+ m_actions.runHooked = new QAction(tr("Open with &VFS"), ui->filetree);
m_actions.preview = new QAction(tr("&Preview"), ui->filetree);
m_actions.explore = new QAction(tr("Open in &Explorer"), ui->filetree);
m_actions.rename = new QAction(tr("&Rename"), ui->filetree);
@@ -32,6 +34,7 @@ FileTreeTab::FileTreeTab(ModInfoDialogTabContext cx)
connect(m_actions.newFolder, &QAction::triggered, [&]{ onCreateDirectory(); });
connect(m_actions.open, &QAction::triggered, [&]{ onOpen(); });
+ connect(m_actions.runHooked, &QAction::triggered, [&]{ onRunHooked(); });
connect(m_actions.preview, &QAction::triggered, [&]{ onPreview(); });
connect(m_actions.explore, &QAction::triggered, [&]{ onExplore(); });
connect(m_actions.rename, &QAction::triggered, [&]{ onRename(); });
@@ -44,6 +47,12 @@ FileTreeTab::FileTreeTab(ModInfoDialogTabContext cx)
connect(
ui->filetree, &QTreeView::customContextMenuRequested,
[&](const QPoint& pos){ onContextMenu(pos); });
+
+ // disable renaming on double click, open the file instead
+ ui->filetree->setEditTriggers(
+ ui->filetree->editTriggers() & (~QAbstractItemView::DoubleClicked));
+
+ connect(ui->filetree, &QTreeView::activated, [&](auto&&){ onActivated(); });
}
void FileTreeTab::clear()
@@ -54,6 +63,18 @@ void FileTreeTab::clear()
setHasData(true);
}
+void FileTreeTab::saveState(Settings& s)
+{
+ s.geometry().saveState(ui->filetree->header());
+}
+
+void FileTreeTab::restoreState(const Settings& s)
+{
+ if (!s.geometry().restoreState(ui->filetree->header())) {
+ ui->filetree->sortByColumn(0, Qt::AscendingOrder);
+ }
+}
+
void FileTreeTab::update()
{
const auto rootPath = mod().absolutePath();
@@ -120,6 +141,28 @@ void FileTreeTab::onCreateDirectory()
ui->filetree->edit(newIndex);
}
+void FileTreeTab::onActivated()
+{
+ auto selection = singleSelection();
+ if (!selection.isValid()) {
+ return;
+ }
+
+ // Don't open explorer on directories as we just want them to be expanded instead.
+ if (m_fs->isDir(selection)) {
+ return;
+ }
+
+ const auto path = m_fs->filePath(selection);
+ const auto tryPreview = core().settings().interface().doubleClicksOpenPreviews();
+
+ if (tryPreview && canPreviewFile(plugin(), false, path)) {
+ onPreview();
+ } else {
+ onOpen();
+ }
+}
+
void FileTreeTab::onOpen()
{
auto selection = singleSelection();
@@ -127,7 +170,27 @@ void FileTreeTab::onOpen()
return;
}
- shell::OpenFile(m_fs->filePath(selection));
+ const auto path = m_fs->filePath(selection);
+ core().processRunner()
+ .setFromFile(parentWidget(), path)
+ .setHooked(false)
+ .setWaitForCompletion()
+ .run();
+}
+
+void FileTreeTab::onRunHooked()
+{
+ auto selection = singleSelection();
+ if (!selection.isValid()) {
+ return;
+ }
+
+ const auto path = m_fs->filePath(selection);
+ core().processRunner()
+ .setFromFile(parentWidget(), path)
+ .setHooked(true)
+ .setWaitForCompletion()
+ .run();
}
void FileTreeTab::onPreview()
@@ -145,9 +208,9 @@ void FileTreeTab::onExplore()
auto selection = singleSelection();
if (selection.isValid()) {
- shell::ExploreFile(m_fs->filePath(selection));
+ shell::Explore(m_fs->filePath(selection));
} else {
- shell::ExploreFile(mod().absolutePath());
+ shell::Explore(mod().absolutePath());
}
}
@@ -203,7 +266,7 @@ void FileTreeTab::onUnhide()
void FileTreeTab::onOpenInExplorer()
{
- shell::ExploreFile(mod().absolutePath());
+ shell::Explore(mod().absolutePath());
}
bool FileTreeTab::deleteFile(const QModelIndex& index)
@@ -230,19 +293,19 @@ bool FileTreeTab::deleteFileRecursive(const QModelIndex& parent)
if (m_fs->isDir(index)) {
if (!deleteFileRecursive(index)) {
- qCritical() << "failed to delete" << m_fs->fileName(index);
+ log::error("failed to delete {}", m_fs->fileName(index));
return false;
}
} else {
if (!m_fs->remove(index)) {
- qCritical() << "failed to delete", m_fs->fileName(index);
+ log::error("failed to delete {}", m_fs->fileName(index));
return false;
}
}
}
if (!m_fs->remove(parent)) {
- qCritical() << "failed to delete" << m_fs->fileName(parent);
+ log::error("failed to delete {}", m_fs->fileName(parent));
return false;
}
@@ -256,9 +319,9 @@ void FileTreeTab::changeVisibility(bool visible)
bool changed = false;
bool stop = false;
- qDebug().nospace()
- << (visible ? "unhiding" : "hiding") << " "
- << selection.size() << " filetree files";
+ log::debug(
+ "{} {} filetree files",
+ (visible ? "unhiding" : "hiding"), selection.size());
QFlags<FileRenamer::RenameFlags> flags =
(visible ? FileRenamer::UNHIDE : FileRenamer::HIDE);
@@ -279,13 +342,13 @@ void FileTreeTab::changeVisibility(bool visible)
if (visible) {
if (!canUnhideFile(false, path)) {
- qDebug().nospace() << "cannot unhide " << path << ", skipping";
+ log::debug("cannot unhide {}, skipping", path);
continue;
}
result = unhideFile(renamer, path);
} else {
if (!canHideFile(false, path)) {
- qDebug().nospace() << "cannot hide " << path << ", skipping";
+ log::debug("cannot hide {}, skipping", path);
continue;
}
result = hideFile(renamer, path);
@@ -311,7 +374,7 @@ void FileTreeTab::changeVisibility(bool visible)
}
}
- qDebug().nospace() << (visible ? "unhiding" : "hiding") << " filetree files done";
+ log::debug("{} filetree files done", (visible ? "unhiding" : "hiding"));
if (changed) {
if (origin()) {
@@ -326,75 +389,52 @@ void FileTreeTab::onContextMenu(const QPoint &pos)
QMenu menu(ui->filetree);
- bool enableNewFolder = true;
- bool enableOpen = true;
- bool enablePreview = true;
- bool enableExplore = true;
- bool enableRename = true;
- bool enableDelete = true;
- bool enableHide = true;
- bool enableUnhide = true;
+ bool enableNewFolder = false;
+ bool enableRun = false;
+ bool enableOpen = false;
+ bool enablePreview = false;
+ bool enableExplore = false;
+ bool enableRename = false;
+ bool enableDelete = false;
+ bool enableHide = false;
+ bool enableUnhide = false;
if (selection.size() == 0) {
// no selection, only new folder and explore
- enableOpen = false;
- enablePreview = false;
- enableRename = false;
- enableDelete = false;
- enableHide = false;
- enableUnhide = false;
+ enableNewFolder = true;
+ enableExplore = true;
} else if (selection.size() == 1) {
// single selection
+ enableNewFolder = true;
+ enableRename = true;
+ enableDelete = true;
// only enable open action if a file is selected
bool hasFiles = false;
- for (auto index : selection) {
- if (m_fs->fileInfo(index).isFile()) {
- hasFiles = true;
- break;
- }
- }
-
- if (!hasFiles) {
- enableOpen = false;
- enablePreview = false;
- }
-
const QString fileName = m_fs->fileName(selection[0]);
- if (!canPreviewFile(plugin(), false, fileName)) {
- enablePreview = false;
- }
-
- if (!canExploreFile(false, fileName)) {
- enableExplore = false;
- }
-
- if (!canHideFile(false, fileName)) {
- enableHide = false;
+ if (m_fs->fileInfo(selection[0]).isFile()) {
+ if (canRunFile(false, fileName)) {
+ enableRun = true;
+ } else if (canOpenFile(false, fileName)) {
+ enableOpen = true;
+ }
}
- if (!canUnhideFile(false, fileName)) {
- enableUnhide = false;
- }
+ enablePreview = canPreviewFile(plugin(), false, fileName);
+ enableExplore = canExploreFile(false, fileName);
+ enableHide = canHideFile(false, fileName);
+ enableUnhide = canUnhideFile(false, fileName);
} else {
- // this is a multiple selection, don't show open action so users don't open
- // a thousand files
- enableOpen = false;
- enablePreview = false;
-
- // can't explore multiple files
- enableExplore = false;
-
- // can't rename multiple files
- enableRename = false;
+ // this is a multiple selection, don't show open or explore actions so users
+ // don't open a thousand files
+ enableNewFolder = true;
+ enableDelete = true;
if (selection.size() < max_scan_for_context_menu) {
// if the number of selected items is low, checking them to accurately
// show the menu items is worth it
- enableHide = false;
- enableUnhide = false;
for (const auto& index : selection) {
const QString fileName = m_fs->fileName(index);
@@ -415,29 +455,85 @@ void FileTreeTab::onContextMenu(const QPoint &pos)
}
}
- menu.addAction(m_actions.newFolder);
- m_actions.newFolder->setEnabled(enableNewFolder);
+ bool enableRunHooked = false;
- menu.addAction(m_actions.open);
- m_actions.open->setEnabled(enableOpen);
+ if (enableRun || enableOpen) {
+ if (auto* p=core().currentProfile()) {
+ if (mod().canBeEnabled()) {
+ const auto index = ModInfo::getIndex(mod().name());
+ if (index == UINT_MAX) {
+ log::error("mod '{}' not found (filetree)", mod().name());
+ } else {
+ enableRunHooked = p->modEnabled(index);
+ }
+ }
+ }
+ }
+
+ if (enableRun) {
+ m_actions.open->setText(tr("&Execute"));
+ m_actions.runHooked->setText(tr("Execute with &VFS"));
+ } else if (enableOpen) {
+ m_actions.open->setText(tr("&Open"));
+ m_actions.runHooked->setText(tr("Open with &VFS"));
+ }
- menu.addAction(m_actions.preview);
m_actions.preview->setEnabled(enablePreview);
+ if ((enableRun || enableOpen) && enablePreview) {
+ if (Settings::instance().interface().doubleClicksOpenPreviews()) {
+ menu.addAction(m_actions.preview);
+ menu.addAction(m_actions.open);
+ } else {
+ menu.addAction(m_actions.open);
+ menu.addAction(m_actions.preview);
+ }
+ } else {
+ if (enableOpen || enableRun) {
+ menu.addAction(m_actions.open);
+ }
+
+ if (enablePreview) {
+ menu.addAction(m_actions.preview);
+ }
+ }
+
+ if (enableRunHooked) {
+ menu.addAction(m_actions.runHooked);
+ }
+
menu.addAction(m_actions.explore);
m_actions.explore->setEnabled(enableExplore);
+ menu.addSeparator();
+
+ menu.addAction(m_actions.newFolder);
+ m_actions.newFolder->setEnabled(enableNewFolder);
+
menu.addAction(m_actions.rename);
m_actions.rename->setEnabled(enableRename);
menu.addAction(m_actions.del);
m_actions.del->setEnabled(enableDelete);
+ menu.addSeparator();
+
menu.addAction(m_actions.hide);
m_actions.hide->setEnabled(enableHide);
menu.addAction(m_actions.unhide);
m_actions.unhide->setEnabled(enableUnhide);
+ if (enableOpen || enableRun || enablePreview) {
+ // bold the first option, unbold all the others
+ for (int i=0; i<menu.actions().size(); ++i) {
+ if (auto* a=menu.actions()[i]) {
+ auto f = a->font();
+ f.setBold(i == 0);
+ a->setFont(f);
+ }
+ }
+ }
+
menu.exec(ui->filetree->viewport()->mapToGlobal(pos));
}
diff --git a/src/modinfodialogfiletree.h b/src/modinfodialogfiletree.h
index f9fa62d4..c3c84ed4 100644
--- a/src/modinfodialogfiletree.h
+++ b/src/modinfodialogfiletree.h
@@ -2,34 +2,42 @@
#define MODINFODIALOGFILETREE_H
#include "modinfodialogtab.h"
+#include <QFileSystemModel>
class FileTreeTab : public ModInfoDialogTab
{
+ Q_OBJECT;
+
public:
FileTreeTab(ModInfoDialogTabContext cx);
void clear() override;
+ void saveState(Settings& s);
+ void restoreState(const Settings& s);
void update() override;
bool deleteRequested() override;
private:
struct Actions
{
- QAction *newFolder = nullptr;
- QAction *open = nullptr;
- QAction *preview = nullptr;
- QAction *explore = nullptr;
- QAction *rename = nullptr;
- QAction *del = nullptr;
- QAction *hide = nullptr;
- QAction *unhide = nullptr;
+ QAction* newFolder = nullptr;
+ QAction* open = nullptr;
+ QAction* runHooked = nullptr;
+ QAction* preview = nullptr;
+ QAction* explore = nullptr;
+ QAction* rename = nullptr;
+ QAction* del = nullptr;
+ QAction* hide = nullptr;
+ QAction* unhide = nullptr;
};
QFileSystemModel* m_fs;
Actions m_actions;
void onCreateDirectory();
+ void onActivated();
void onOpen();
+ void onRunHooked();
void onPreview();
void onExplore();
void onRename();
diff --git a/src/modinfodialogfwd.h b/src/modinfodialogfwd.h
index 9ede766f..2147fc04 100644
--- a/src/modinfodialogfwd.h
+++ b/src/modinfodialogfwd.h
@@ -23,6 +23,7 @@ enum class ModInfoTabIDs
class PluginContainer;
bool canPreviewFile(PluginContainer& pluginContainer, bool isArchive, const QString& filename);
+bool canRunFile(bool isArchive, const QString& filename);
bool canOpenFile(bool isArchive, const QString& filename);
bool canExploreFile(bool isArchive, const QString& filename);
bool canHideFile(bool isArchive, const QString& filename);
diff --git a/src/modinfodialogimages.cpp b/src/modinfodialogimages.cpp
index 69866902..48b3bb3b 100644
--- a/src/modinfodialogimages.cpp
+++ b/src/modinfodialogimages.cpp
@@ -2,7 +2,9 @@
#include "ui_modinfodialog.h"
#include "settings.h"
#include "utility.h"
+#include <log.h>
+using namespace MOBase;
using namespace ImagesTabHelpers;
QSize resizeWithAspectRatio(const QSize& original, const QSize& available)
@@ -51,6 +53,8 @@ ImagesTab::ImagesTab(ModInfoDialogTabContext cx) :
ui->tabImagesSplitter->setStretchFactor(0, 0);
ui->tabImagesSplitter->setStretchFactor(1, 1);
+ ui->previewPluginButton->setEnabled(false);
+
ui->imagesThumbnails->setTab(this);
ui->imagesScrollerVBar->setTab(this);
@@ -63,6 +67,7 @@ ImagesTab::ImagesTab(ModInfoDialogTabContext cx) :
connect(ui->imagesExplore, &QAbstractButton::clicked, [&]{ onExplore(); });
connect(ui->imagesShowDDS, &QCheckBox::toggled, [&]{ onShowDDS(); });
+ connect(ui->previewPluginButton, &QAbstractButton::clicked, [&] { onPreviewButton(); });
ui->imagesShowDDS->setEnabled(m_ddsAvailable);
@@ -128,18 +133,14 @@ void ImagesTab::update()
void ImagesTab::saveState(Settings& s)
{
- s.directInterface().setValue(
- "mod_info_dialog_images_show_dds", m_ddsEnabled);
-
- saveWidgetState(s.directInterface(), ui->tabImagesSplitter);
+ s.widgets().saveChecked(ui->imagesShowDDS);
+ s.geometry().saveState(ui->tabImagesSplitter);
}
void ImagesTab::restoreState(const Settings& s)
{
- ui->imagesShowDDS->setChecked(s.directInterface()
- .value("mod_info_dialog_images_show_dds", false).toBool());
-
- restoreWidgetState(s.directInterface(), ui->tabImagesSplitter);
+ s.widgets().restoreChecked(ui->imagesShowDDS);
+ s.geometry().restoreState(ui->tabImagesSplitter);
}
void ImagesTab::checkFiltering()
@@ -253,13 +254,34 @@ void ImagesTab::select(std::size_t i, Visibility v)
ui->imagesPath->setText(QDir::toNativeSeparators(f->path()));
ui->imagesExplore->setEnabled(true);
+ if (plugin().previewGenerator().previewSupported(QString::fromStdWString(std::filesystem::path(f->path().toStdWString()).extension().wstring()).remove(0,1)))
+ ui->previewPluginButton->setEnabled(true);
+ else
+ ui->previewPluginButton->setEnabled(false);
ui->imagesSize->setText(dimensionString(f->original().size()));
- m_image->setImage(f->original());
+ if (f->original().isNull()) {
+ m_image->clear();
+
+ QImage image(300, 100, QImage::Format_RGBA64);
+ QPainter paint;
+ paint.begin(&image);
+ paint.fillRect(0, 0, 300, 100, QBrush(QColor(0, 0, 0, 255)));
+ paint.setPen(m_theme.textColor);
+ paint.setFont(m_theme.font);
+ paint.drawImage(QPoint(150-16, 50-20-16), QImage(":/MO/gui/warning"));
+ const auto flags = Qt::AlignHCenter | Qt::AlignVCenter | Qt::TextWordWrap;
+ paint.drawText(0, 46, 300, 54, flags, "This image format is not supported by Qt, but the preview plugin may be able to display it. Use the button above.");
+ paint.end();
+
+ m_image->setImage(image);
+ } else
+ m_image->setImage(f->original());
ensureVisible(i, v);
} else {
ui->imagesPath->clear();
ui->imagesExplore->setEnabled(false);
+ ui->previewPluginButton->setEnabled(false);
ui->imagesSize->clear();
m_image->clear();
}
@@ -549,7 +571,7 @@ void ImagesTab::showTooltip(QHelpEvent* e)
void ImagesTab::onExplore()
{
if (auto* f=m_files.selectedFile()) {
- MOBase::shell::ExploreFile(f->path());
+ shell::Explore(f->path());
}
}
@@ -562,6 +584,11 @@ void ImagesTab::onShowDDS()
}
}
+void ImagesTab::onPreviewButton()
+{
+ core().previewFileWithAlternatives(parentWidget(), m_files.selectedFile()->path());
+}
+
void ImagesTab::onFilterChanged()
{
update();
@@ -896,10 +923,9 @@ void File::ensureOriginalLoaded()
QImageReader reader(m_path);
if (!reader.read(&m_original)) {
- qCritical().noquote().nospace()
- << "failed to load '" << m_path << "'\n"
- << reader.errorString() << " "
- << "(error " << static_cast<int>(reader.error()) << ")";
+ log::error(
+ "failed to load '{}'\n{} (error {})",
+ m_path, reader.errorString(), static_cast<int>(reader.error()));
m_failed = true;
}
@@ -961,13 +987,17 @@ void File::load(const Geometry& geo)
ensureOriginalLoaded();
if (m_failed) {
- return;
- }
+ QImage warning(":/MO/gui/warning");
+ const auto scaledSize = geo.scaledImageSize(warning.size());
- const auto scaledSize = geo.scaledImageSize(m_original.size());
+ m_thumbnail = warning.scaled(
+ scaledSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
+ } else {
+ const auto scaledSize = geo.scaledImageSize(m_original.size());
- m_thumbnail = m_original.scaled(
- scaledSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
+ m_thumbnail = m_original.scaled(
+ scaledSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
+ }
}
diff --git a/src/modinfodialogimages.h b/src/modinfodialogimages.h
index 8d9b965b..5717b8b9 100644
--- a/src/modinfodialogimages.h
+++ b/src/modinfodialogimages.h
@@ -4,6 +4,8 @@
#include "modinfodialogtab.h"
#include "filterwidget.h"
#include <QScrollBar>
+#include "plugincontainer.h"
+#include "organizercore.h"
class ImagesTab;
@@ -359,6 +361,7 @@ private:
void showTooltip(QHelpEvent* e);
void onExplore();
void onShowDDS();
+ void onPreviewButton();
void onFilterChanged();
void select(std::size_t i, Visibility v=Visibility::Full);
diff --git a/src/modinfodialognexus.cpp b/src/modinfodialognexus.cpp
index e606525c..59bfe930 100644
--- a/src/modinfodialognexus.cpp
+++ b/src/modinfodialognexus.cpp
@@ -6,8 +6,9 @@
#include "bbcode.h"
#include <versioninfo.h>
#include <utility.h>
+#include <log.h>
-namespace shell = MOBase::shell;
+using namespace MOBase;
bool isValidModID(int id)
{
@@ -18,7 +19,7 @@ NexusTab::NexusTab(ModInfoDialogTabContext cx) :
ModInfoDialogTab(std::move(cx)), m_requestStarted(false), m_loading(false)
{
ui->modID->setValidator(new QIntValidator(ui->modID));
- ui->endorse->setVisible(core().settings().endorsementIntegration());
+ ui->endorse->setVisible(core().settings().nexus().endorsementIntegration());
connect(ui->modID, &QLineEdit::editingFinished, [&]{ onModIDChanged(); });
connect(
@@ -94,7 +95,7 @@ void NexusTab::update()
connect(
page, &NexusTabWebpage::linkClicked,
- [&](const QUrl& url){ shell::OpenLink(url); });
+ [&](const QUrl& url){ shell::Open(url); });
ui->endorse->setEnabled(
(mod().endorsedState() == ModInfo::ENDORSED_FALSE) ||
@@ -350,7 +351,7 @@ void NexusTab::onRefreshBrowser()
mod().setLastNexusQuery(QDateTime::fromSecsSinceEpoch(0));
updateWebpage();
} else {
- qInfo("Mod has no valid Nexus ID, info can't be updated.");
+ log::info("Mod has no valid Nexus ID, info can't be updated.");
}
}
@@ -362,7 +363,7 @@ void NexusTab::onVisitNexus()
const QString nexusLink = NexusInterface::instance(&plugin())
->getModURL(modID, mod().getGameName());
- shell::OpenLink(QUrl(nexusLink));
+ shell::Open(QUrl(nexusLink));
}
}
@@ -411,6 +412,6 @@ void NexusTab::onVisitCustomURL()
{
const auto url = mod().parseCustomURL();
if (url.isValid()) {
- shell::OpenLink(url);
+ shell::Open(url);
}
}
diff --git a/src/modinfodialognexus.h b/src/modinfodialognexus.h
index 7f894dbf..1cfa2057 100644
--- a/src/modinfodialognexus.h
+++ b/src/modinfodialognexus.h
@@ -32,6 +32,8 @@ signals:
class NexusTab : public ModInfoDialogTab
{
+ Q_OBJECT;
+
public:
NexusTab(ModInfoDialogTabContext cx);
diff --git a/src/modinfodialogtab.h b/src/modinfodialogtab.h
index c43fa076..d0a58574 100644
--- a/src/modinfodialogtab.h
+++ b/src/modinfodialogtab.h
@@ -253,32 +253,6 @@ protected:
//
void setFocus();
-
- // saves the sate of the given widget in geometry/modinfodialog_[objectname]
- //
- // this needs to be a template because saveState() and restoreState() are
- // not in QWidget, but they're in various widgets
- //
- template <class Widget>
- void saveWidgetState(QSettings& s, Widget* w)
- {
- s.setValue(settingName(w), w->saveState());
- }
-
- // restores the sate of the given widget from
- // geometry/modinfodialog_[objectname]
- //
- // this needs to be a template because saveState() and restoreState() are
- // not in QWidget, but they're in various widgets
- //
- template <class Widget>
- void restoreWidgetState(const QSettings& s, Widget* w)
- {
- if (s.contains(settingName(w))) {
- w->restoreState(s.value(settingName(w)).toByteArray());
- }
- }
-
private:
// core
OrganizerCore& m_core;
@@ -303,14 +277,6 @@ private:
// true if the tab has never been selected for the current mod
bool m_firstActivation;
-
-
- // used by saveWidgetState() and restoreWidgetState()
- //
- QString settingName(QWidget* w)
- {
- return "geometry/modinfodialog_" + w->objectName();
- }
};
diff --git a/src/modinfodialogtextfiles.cpp b/src/modinfodialogtextfiles.cpp
index 7a09fa4e..fdf12568 100644
--- a/src/modinfodialogtextfiles.cpp
+++ b/src/modinfodialogtextfiles.cpp
@@ -176,12 +176,12 @@ void GenericFilesTab::update()
void GenericFilesTab::saveState(Settings& s)
{
- saveWidgetState(s.directInterface(), m_splitter);
+ s.geometry().saveState(m_splitter);
}
void GenericFilesTab::restoreState(const Settings& s)
{
- restoreWidgetState(s.directInterface(), m_splitter);
+ s.geometry().restoreState(m_splitter);
}
void GenericFilesTab::onSelection(
diff --git a/src/modinfoforeign.cpp b/src/modinfoforeign.cpp
index b599d4eb..84199eae 100644
--- a/src/modinfoforeign.cpp
+++ b/src/modinfoforeign.cpp
@@ -8,11 +8,6 @@
using namespace MOBase;
using namespace MOShared;
-QString ModInfoForeign::name() const
-{
- return m_Name;
-}
-
QDateTime ModInfoForeign::creationTime() const
{
return m_CreationTime;
@@ -55,15 +50,18 @@ ModInfoForeign::ModInfoForeign(const QString &modName,
PluginContainer *pluginContainer)
: ModInfoWithConflictInfo(pluginContainer, directoryStructure),
m_ReferenceFile(referenceFile), m_Archives(archives) {
- m_CreationTime = QFileInfo(referenceFile).created();
+ m_CreationTime = QFileInfo(referenceFile).birthTime();
switch (modType) {
case ModInfo::EModType::MOD_DLC:
m_Name = tr("DLC: ") + modName;
+ m_InternalName = QString("DLC: ") + modName;
break;
case ModInfo::EModType::MOD_CC:
m_Name = tr("Creation Club: ") + modName;
+ m_InternalName = QString("Creation Club: ") + modName;
break;
default:
m_Name = tr("Unmanaged: ") + modName;
+ m_InternalName = QString("Unmanaged: ") + modName;
}
}
diff --git a/src/modinfoforeign.h b/src/modinfoforeign.h
index 72fbb04f..3308d42f 100644
--- a/src/modinfoforeign.h
+++ b/src/modinfoforeign.h
@@ -35,8 +35,8 @@ public:
virtual void track(bool) {}
virtual void parseNexusInfo() {}
virtual bool isEmpty() const { return false; }
- virtual QString name() const;
- virtual QString internalName() const { return name(); }
+ virtual QString name() const { return m_Name; }
+ virtual QString internalName() const { return m_InternalName; }
virtual QString comments() const { return ""; }
virtual QString notes() const { return ""; }
virtual QDateTime creationTime() const;
@@ -72,6 +72,7 @@ protected:
private:
QString m_Name;
+ QString m_InternalName;
QString m_ReferenceFile;
QStringList m_Archives;
QDateTime m_CreationTime;
diff --git a/src/modinfooverwrite.cpp b/src/modinfooverwrite.cpp
index 37c8c650..9a6a22c1 100644
--- a/src/modinfooverwrite.cpp
+++ b/src/modinfooverwrite.cpp
@@ -23,7 +23,7 @@ bool ModInfoOverwrite::isEmpty() const
QString ModInfoOverwrite::absolutePath() const
{
- return Settings::instance().getOverwriteDirectory();
+ return Settings::instance().paths().overwrite();
}
std::vector<ModInfo::EFlag> ModInfoOverwrite::getFlags() const
@@ -37,6 +37,15 @@ std::vector<ModInfo::EFlag> ModInfoOverwrite::getFlags() const
return result;
}
+std::vector<ModInfo::EConflictFlag> ModInfoOverwrite::getConflictFlags() const
+{
+ std::vector<ModInfo::EConflictFlag> result;
+ result.push_back(FLAG_OVERWRITE_CONFLICT);
+ for (auto flag : ModInfoWithConflictInfo::getConflictFlags())
+ result.push_back(flag);
+ return result;
+}
+
int ModInfoOverwrite::getHighlight() const
{
int highlight = (isValid() ? HIGHLIGHT_IMPORTANT : HIGHLIGHT_INVALID) | HIGHLIGHT_CENTER;
diff --git a/src/modinfooverwrite.h b/src/modinfooverwrite.h
index c5f58c2e..ecbdbe3d 100644
--- a/src/modinfooverwrite.h
+++ b/src/modinfooverwrite.h
@@ -51,6 +51,7 @@ public:
virtual QDateTime getExpires() const { return QDateTime(); }
virtual std::vector<QString> getIniTweaks() const { return std::vector<QString>(); }
virtual std::vector<ModInfo::EFlag> getFlags() const;
+ virtual std::vector<ModInfo::EConflictFlag> getConflictFlags() const;
virtual int getHighlight() const;
virtual QString getDescription() const;
virtual int getNexusFileStatus() const { return 0; }
diff --git a/src/modinforegular.cpp b/src/modinforegular.cpp
index 448447e1..3cff914a 100644
--- a/src/modinforegular.cpp
+++ b/src/modinforegular.cpp
@@ -39,7 +39,7 @@ ModInfoRegular::ModInfoRegular(PluginContainer *pluginContainer, const IPluginGa
, m_NexusBridge(pluginContainer)
{
testValid();
- m_CreationTime = QFileInfo(path.absolutePath()).created();
+ m_CreationTime = QFileInfo(path.absolutePath()).birthTime();
// read out the meta-file for information
readMeta();
if (m_GameName.compare(game->gameShortName(), Qt::CaseInsensitive) != 0)
@@ -68,8 +68,7 @@ ModInfoRegular::~ModInfoRegular()
try {
saveMeta();
} catch (const std::exception &e) {
- qCritical("failed to save meta information for \"%s\": %s",
- qUtf8Printable(m_Name), e.what());
+ log::error("failed to save meta information for \"{}\": {}", m_Name, e.what());
}
}
@@ -258,14 +257,14 @@ void ModInfoRegular::saveMeta()
if (metaFile.status() == QSettings::NoError) {
m_MetaInfoChanged = false;
} else {
- qCritical()
- << QString("failed to write %1/meta.ini: error %2")
- .arg(absolutePath()).arg(metaFile.status());
+ log::error(
+ "failed to write {}/meta.ini: error {}",
+ absolutePath(), metaFile.status());
}
} else {
- qCritical()
- << QString("failed to write %1/meta.ini: error %2")
- .arg(absolutePath()).arg(metaFile.status());
+ log::error(
+ "failed to write {}/meta.ini: error {}",
+ absolutePath(), metaFile.status());
}
}
}
@@ -425,14 +424,13 @@ bool ModInfoRegular::setName(const QString &name)
return false;
}
if (!modDir.rename(tempName, name)) {
- qCritical("rename to final name failed after successful rename to intermediate name");
+ log::error("rename to final name failed after successful rename to intermediate name");
modDir.rename(tempName, m_Name);
return false;
}
} else {
if (!shellRename(modDir.absoluteFilePath(m_Name), modDir.absoluteFilePath(name))) {
- qCritical("failed to rename mod %s (errorcode %d)",
- qUtf8Printable(name), ::GetLastError());
+ log::error("failed to rename mod {} (errorcode {})", name, ::GetLastError());
return false;
}
}
@@ -633,7 +631,7 @@ std::vector<ModInfo::EFlag> ModInfoRegular::getFlags() const
std::vector<ModInfo::EFlag> result = ModInfoWithConflictInfo::getFlags();
if ((m_NexusID > 0) &&
(endorsedState() == ENDORSED_FALSE) &&
- Settings::instance().endorsementIntegration()) {
+ Settings::instance().nexus().endorsementIntegration()) {
result.push_back(ModInfo::FLAG_NOTENDORSED);
}
if ((m_NexusID > 0) &&
@@ -885,8 +883,9 @@ std::vector<QString> ModInfoRegular::getIniTweaks() const
int numTweaks = metaFile.beginReadArray("INI Tweaks");
if (numTweaks != 0) {
- qDebug("%d active ini tweaks in %s",
- numTweaks, QDir::toNativeSeparators(metaFileName).toUtf8().constData());
+ log::debug(
+ "{} active ini tweaks in {}",
+ numTweaks, QDir::toNativeSeparators(metaFileName));
}
for (int i = 0; i < numTweaks; ++i) {
diff --git a/src/modinfoseparator.h b/src/modinfoseparator.h
index 3560d786..4b1e5217 100644
--- a/src/modinfoseparator.h
+++ b/src/modinfoseparator.h
@@ -6,6 +6,8 @@
class ModInfoSeparator:
public ModInfoRegular
{
+ Q_OBJECT;
+
friend class ModInfo;
public:
diff --git a/src/modinfowithconflictinfo.cpp b/src/modinfowithconflictinfo.cpp
index 7a2b0071..f5a243ae 100644
--- a/src/modinfowithconflictinfo.cpp
+++ b/src/modinfowithconflictinfo.cpp
@@ -14,9 +14,9 @@ void ModInfoWithConflictInfo::clearCaches()
m_LastConflictCheck = QTime();
}
-std::vector<ModInfo::EFlag> ModInfoWithConflictInfo::getFlags() const
+std::vector<ModInfo::EConflictFlag> ModInfoWithConflictInfo::getConflictFlags() const
{
- std::vector<ModInfo::EFlag> result;
+ std::vector<ModInfo::EConflictFlag> result;
switch (isConflicted()) {
case CONFLICT_MIXED: {
result.push_back(ModInfo::FLAG_CONFLICT_MIXED);
diff --git a/src/modinfowithconflictinfo.h b/src/modinfowithconflictinfo.h
index 13d87d25..9869bc5a 100644
--- a/src/modinfowithconflictinfo.h
+++ b/src/modinfowithconflictinfo.h
@@ -12,7 +12,8 @@ public:
ModInfoWithConflictInfo(PluginContainer *pluginContainer, MOShared::DirectoryEntry **directoryStructure);
- std::vector<ModInfo::EFlag> getFlags() const;
+ std::vector<ModInfo::EConflictFlag> getConflictFlags() const;
+ virtual std::vector<ModInfo::EFlag> getFlags() const { return std::vector<ModInfo::EFlag>(); };
/**
* @brief clear all caches held for this mod
diff --git a/src/modlist.cpp b/src/modlist.cpp
index 7b71355c..5cfa7022 100644
--- a/src/modlist.cpp
+++ b/src/modlist.cpp
@@ -61,22 +61,23 @@ ModList::ModList(PluginContainer *pluginContainer, QObject *parent)
, m_Profile(nullptr)
, m_NexusInterface(nullptr)
, m_Modified(false)
+ , m_InNotifyChange(false)
, m_FontMetrics(QFont())
, m_DropOnItems(false)
, m_PluginContainer(pluginContainer)
{
- m_ContentIcons[ModInfo::CONTENT_PLUGIN] = std::make_tuple(":/MO/gui/content/plugin", tr("Game Plugins (ESP/ESM/ESL)"));
- m_ContentIcons[ModInfo::CONTENT_INTERFACE] = std::make_tuple(":/MO/gui/content/interface", tr("Interface"));
- m_ContentIcons[ModInfo::CONTENT_MESH] = std::make_tuple(":/MO/gui/content/mesh", tr("Meshes"));
- m_ContentIcons[ModInfo::CONTENT_BSA] = std::make_tuple(":/MO/gui/content/bsa", tr("Bethesda Archive"));
- m_ContentIcons[ModInfo::CONTENT_SCRIPT] = std::make_tuple(":/MO/gui/content/script", tr("Scripts (Papyrus)"));
- m_ContentIcons[ModInfo::CONTENT_SKSE] = std::make_tuple(":/MO/gui/content/skse", tr("Script Extender Plugin"));
- m_ContentIcons[ModInfo::CONTENT_SKYPROC] = std::make_tuple(":/MO/gui/content/skyproc", tr("SkyProc Patcher"));
- m_ContentIcons[ModInfo::CONTENT_SOUND] = std::make_tuple(":/MO/gui/content/sound", tr("Sound or Music"));
- m_ContentIcons[ModInfo::CONTENT_TEXTURE] = std::make_tuple(":/MO/gui/content/texture", tr("Textures"));
- m_ContentIcons[ModInfo::CONTENT_MCM] = std::make_tuple(":/MO/gui/content/menu", tr("MCM Configuration"));
- m_ContentIcons[ModInfo::CONTENT_INI] = std::make_tuple(":/MO/gui/content/inifile", tr("INI files"));
- m_ContentIcons[ModInfo::CONTENT_MODGROUP] = std::make_tuple(":/MO/gui/content/modgroup", tr("ModGroup files"));
+ m_ContentIcons[ModInfo::CONTENT_PLUGIN] = std::make_tuple(":/MO/gui/content/plugin", QT_TR_NOOP("Game Plugins (ESP/ESM/ESL)"));
+ m_ContentIcons[ModInfo::CONTENT_INTERFACE] = std::make_tuple(":/MO/gui/content/interface", QT_TR_NOOP("Interface"));
+ m_ContentIcons[ModInfo::CONTENT_MESH] = std::make_tuple(":/MO/gui/content/mesh", QT_TR_NOOP("Meshes"));
+ m_ContentIcons[ModInfo::CONTENT_BSA] = std::make_tuple(":/MO/gui/content/bsa", QT_TR_NOOP("Bethesda Archive"));
+ m_ContentIcons[ModInfo::CONTENT_SCRIPT] = std::make_tuple(":/MO/gui/content/script", QT_TR_NOOP("Scripts (Papyrus)"));
+ m_ContentIcons[ModInfo::CONTENT_SKSE] = std::make_tuple(":/MO/gui/content/skse", QT_TR_NOOP("Script Extender Plugin"));
+ m_ContentIcons[ModInfo::CONTENT_SKYPROC] = std::make_tuple(":/MO/gui/content/skyproc", QT_TR_NOOP("SkyProc Patcher"));
+ m_ContentIcons[ModInfo::CONTENT_SOUND] = std::make_tuple(":/MO/gui/content/sound", QT_TR_NOOP("Sound or Music"));
+ m_ContentIcons[ModInfo::CONTENT_TEXTURE] = std::make_tuple(":/MO/gui/content/texture", QT_TR_NOOP("Textures"));
+ m_ContentIcons[ModInfo::CONTENT_MCM] = std::make_tuple(":/MO/gui/content/menu", QT_TR_NOOP("MCM Configuration"));
+ m_ContentIcons[ModInfo::CONTENT_INI] = std::make_tuple(":/MO/gui/content/inifile", QT_TR_NOOP("INI files"));
+ m_ContentIcons[ModInfo::CONTENT_MODGROUP] = std::make_tuple(":/MO/gui/content/modgroup", QT_TR_NOOP("ModGroup files"));
m_LastCheck.start();
}
@@ -158,15 +159,6 @@ QString ModList::getFlagText(ModInfo::EFlag flag, ModInfo::Ptr modInfo) const
output << QString("<i>%1</i>").arg(modInfo->notes());
return output.join("");
}
- case ModInfo::FLAG_CONFLICT_OVERWRITE: return tr("Overwrites loose files");
- case ModInfo::FLAG_CONFLICT_OVERWRITTEN: return tr("Overwritten loose files");
- case ModInfo::FLAG_CONFLICT_MIXED: return tr("Loose files Overwrites & Overwritten");
- case ModInfo::FLAG_CONFLICT_REDUNDANT: return tr("Redundant");
- case ModInfo::FLAG_ARCHIVE_LOOSE_CONFLICT_OVERWRITE: return tr("Overwrites an archive with loose files");
- case ModInfo::FLAG_ARCHIVE_LOOSE_CONFLICT_OVERWRITTEN: return tr("Archive is overwritten by loose files");
- case ModInfo::FLAG_ARCHIVE_CONFLICT_OVERWRITE: return tr("Overwrites another archive file");
- case ModInfo::FLAG_ARCHIVE_CONFLICT_OVERWRITTEN: return tr("Overwritten by another archive file");
- case ModInfo::FLAG_ARCHIVE_CONFLICT_MIXED: return tr("Archive files overwrites & overwritten");
case ModInfo::FLAG_ALTERNATE_GAME: return tr("<br>This mod is for a different game, "
"make sure it's compatible or it could cause crashes.");
case ModInfo::FLAG_TRACKED: return tr("Mod is being tracked on the website");
@@ -175,6 +167,23 @@ QString ModList::getFlagText(ModInfo::EFlag flag, ModInfo::Ptr modInfo) const
}
+QString ModList::getConflictFlagText(ModInfo::EConflictFlag flag, ModInfo::Ptr modInfo) const
+{
+ switch (flag) {
+ case ModInfo::FLAG_CONFLICT_OVERWRITE: return tr("Overwrites loose files");
+ case ModInfo::FLAG_CONFLICT_OVERWRITTEN: return tr("Overwritten loose files");
+ case ModInfo::FLAG_CONFLICT_MIXED: return tr("Loose files Overwrites & Overwritten");
+ case ModInfo::FLAG_CONFLICT_REDUNDANT: return tr("Redundant");
+ case ModInfo::FLAG_ARCHIVE_LOOSE_CONFLICT_OVERWRITE: return tr("Overwrites an archive with loose files");
+ case ModInfo::FLAG_ARCHIVE_LOOSE_CONFLICT_OVERWRITTEN: return tr("Archive is overwritten by loose files");
+ case ModInfo::FLAG_ARCHIVE_CONFLICT_OVERWRITE: return tr("Overwrites another archive file");
+ case ModInfo::FLAG_ARCHIVE_CONFLICT_OVERWRITTEN: return tr("Overwritten by another archive file");
+ case ModInfo::FLAG_ARCHIVE_CONFLICT_MIXED: return tr("Archive files overwrites & overwritten");
+ default: return "";
+ }
+}
+
+
QVariantList ModList::contentsToIcons(const std::vector<ModInfo::EContent> &contents) const
{
QVariantList result;
@@ -198,7 +207,7 @@ QString ModList::contentsToToolTip(const std::vector<ModInfo::EContent> &content
if (contentsSet.find(iter->first) != contentsSet.end()) {
result.append(QString("<tr><td><img src=\"%1\" width=32/></td>"
"<td valign=\"middle\">%2</td></tr>")
- .arg(std::get<0>(iter->second)).arg(std::get<1>(iter->second)));
+ .arg(std::get<0>(iter->second)).arg(tr(std::get<1>(iter->second).toStdString().c_str())));
}
}
result.append("</table>");
@@ -217,7 +226,8 @@ QVariant ModList::data(const QModelIndex &modelIndex, int role) const
if ((role == Qt::DisplayRole) ||
(role == Qt::EditRole)) {
if ((column == COL_FLAGS)
- || (column == COL_CONTENT)) {
+ || (column == COL_CONTENT)
+ || (column == COL_CONFLICTFLAGS)) {
return QVariant();
} else if (column == COL_NAME) {
auto flags = modInfo->getFlags();
@@ -271,11 +281,11 @@ QVariant ModList::data(const QModelIndex &modelIndex, int role) const
int categoryIdx = categoryFactory.getCategoryIndex(category);
return categoryFactory.getCategoryName(categoryIdx);
} catch (const std::exception &e) {
- qCritical("failed to retrieve category name: %s", e.what());
+ log::error("failed to retrieve category name: {}", e.what());
return QString();
}
} else {
- qWarning("category %d doesn't exist (may have been removed)", category);
+ log::warn("category {} doesn't exist (may have been removed)", category);
modInfo->setCategory(category, false);
return QString();
}
@@ -390,7 +400,7 @@ QVariant ModList::data(const QModelIndex &modelIndex, int role) const
return QVariant();
} else if (role == Qt::ForegroundRole) {
if (modInfo->hasFlag(ModInfo::FLAG_SEPARATOR) && modInfo->getColor().isValid()) {
- return Settings::getIdealTextColor(modInfo->getColor());
+ return ColorSettings::idealTextColor(modInfo->getColor());
} else if (column == COL_NAME) {
int highlight = modInfo->getHighlight();
if (highlight & ModInfo::HIGHLIGHT_IMPORTANT)
@@ -416,19 +426,19 @@ QVariant ModList::data(const QModelIndex &modelIndex, int role) const
bool archiveOverwritten = m_ArchiveOverwritten.find(modIndex) != m_ArchiveOverwritten.end();
bool archiveLooseOverwritten = m_ArchiveLooseOverwritten.find(modIndex) != m_ArchiveLooseOverwritten.end();
if (modInfo->getHighlight() & ModInfo::HIGHLIGHT_PLUGIN) {
- return Settings::instance().modlistContainsPluginColor();
+ return Settings::instance().colors().modlistContainsPlugin();
} else if (overwritten || archiveLooseOverwritten) {
- return Settings::instance().modlistOverwritingLooseColor();
+ return Settings::instance().colors().modlistOverwritingLoose();
} else if (overwrite || archiveLooseOverwrite) {
- return Settings::instance().modlistOverwrittenLooseColor();
+ return Settings::instance().colors().modlistOverwrittenLoose();
} else if (archiveOverwritten) {
- return Settings::instance().modlistOverwritingArchiveColor();
+ return Settings::instance().colors().modlistOverwritingArchive();
} else if (archiveOverwrite) {
- return Settings::instance().modlistOverwrittenArchiveColor();
+ return Settings::instance().colors().modlistOverwrittenArchive();
} else if (modInfo->hasFlag(ModInfo::FLAG_SEPARATOR)
&& modInfo->getColor().isValid()
&& ((role != ViewMarkingScrollBar::DEFAULT_ROLE)
- || Settings::instance().colorSeparatorScrollbar())) {
+ || Settings::instance().colors().colorSeparatorScrollbar())) {
return modInfo->getColor();
} else {
return QVariant();
@@ -443,13 +453,22 @@ QVariant ModList::data(const QModelIndex &modelIndex, int role) const
}
return result;
+ } else if (column == COL_CONFLICTFLAGS) {
+ QString result;
+
+ for (ModInfo::EConflictFlag flag : modInfo->getConflictFlags()) {
+ if (result.length() != 0) result += "<br>";
+ result += getConflictFlagText(flag, modInfo);
+ }
+
+ return result;
} else if (column == COL_CONTENT) {
return contentsToToolTip(modInfo->getContents());
} else if (column == COL_NAME) {
try {
return modInfo->getDescription();
} catch (const std::exception &e) {
- qCritical("invalid mod description: %s", e.what());
+ log::error("invalid mod description: {}", e.what());
return QString();
}
} else if (column == COL_VERSION) {
@@ -488,7 +507,7 @@ QVariant ModList::data(const QModelIndex &modelIndex, int role) const
try {
categoryString << "<span style=\"white-space: nowrap;\"><i>" << ToWString(categoryFactory.getCategoryName(categoryFactory.getCategoryIndex(*catIter))) << "</font></span>";
} catch (const std::exception &e) {
- qCritical("failed to generate tooltip: %s", e.what());
+ log::error("failed to generate tooltip: {}", e.what());
return QString();
}
}
@@ -618,8 +637,9 @@ bool ModList::setData(const QModelIndex &index, const QVariant &value, int role)
result = true;
} break;
default: {
- qWarning("edit on column \"%s\" not supported",
- getColumnName(index.column()).toUtf8().constData());
+ log::warn(
+ "edit on column \"{}\" not supported",
+ getColumnName(index.column()).toUtf8().constData());
result = false;
} break;
}
@@ -635,9 +655,9 @@ bool ModList::setData(const QModelIndex &index, const QVariant &value, int role)
try {
m_ModStateChanged(info->name(), newState);
} catch (const std::exception &e) {
- qCritical("failed to invoke state changed notification: %s", e.what());
+ log::error("failed to invoke state changed notification: {}", e.what());
} catch (...) {
- qCritical("failed to invoke state changed notification: unknown exception");
+ log::error("failed to invoke state changed notification: unknown exception");
}
}
@@ -833,7 +853,7 @@ void ModList::modInfoChanged(ModInfo::Ptr info)
emit dataChanged(index(row, 0), index(row, columnCount()));
emit postDataChanged();
} else {
- qCritical("modInfoChanged not called after modInfoAboutToChange");
+ log::error("modInfoChanged not called after modInfoAboutToChange");
}
m_ChangeInfo.name = QString();
}
@@ -998,8 +1018,8 @@ bool ModList::dropURLs(const QMimeData *mimeData, int row, const QModelIndex &pa
ModInfo::Ptr modInfo = ModInfo::getByIndex(row);
QDir modDir = QDir(modInfo->absolutePath());
- QDir allModsDir(Settings::instance().getModDirectory());
- QDir overwriteDir(Settings::instance().getOverwriteDirectory());
+ QDir allModsDir(Settings::instance().paths().mods());
+ QDir overwriteDir(Settings::instance().paths().overwrite());
QStringList sourceList;
QStringList targetList;
@@ -1012,9 +1032,8 @@ bool ModList::dropURLs(const QMimeData *mimeData, int row, const QModelIndex &pa
QString overwriteName = ModInfo::getByIndex(overwriteIndex)->name();
for (auto url : mimeData->urls()) {
- //qDebug("URL drop requested: %s -> %s", qUtf8Printable(url.url()), qUtf8Printable(modDir.canonicalPath()));
if (!url.isLocalFile()) {
- qDebug("URL drop ignored: \"%s\" is not a local file", qUtf8Printable(url.url()));
+ log::debug("URL drop ignored: \"{}\" is not a local file", url.url());
continue;
}
@@ -1034,7 +1053,7 @@ bool ModList::dropURLs(const QMimeData *mimeData, int row, const QModelIndex &pa
originName = overwriteName;
relativePath = overwriteDir.relativeFilePath(sourceFile);
} else {
- qDebug("URL drop ignored: \"%s\" is not a known file to MO", qUtf8Printable(sourceFile));
+ log::debug("URL drop ignored: \"{}\" is not a known file to MO", sourceFile);
continue;
}
@@ -1046,7 +1065,7 @@ bool ModList::dropURLs(const QMimeData *mimeData, int row, const QModelIndex &pa
if (sourceList.count()) {
if (!shellMove(sourceList, targetList)) {
- qDebug("Failed to move file (error %d)", ::GetLastError());
+ log::debug("Failed to move file (error {})", ::GetLastError());
return false;
}
}
@@ -1195,6 +1214,32 @@ bool ModList::removeRows(int row, int count, const QModelIndex &parent)
void ModList::notifyChange(int rowStart, int rowEnd)
{
+ // this function can emit dataChanged(), which can eventually recurse back
+ // here; for example:
+ //
+ // - a filter is active in the mod list, such as "no categories"
+ // - mods are selected and a category is set on them
+ // - these mods get updated here and disappear from the list because they're
+ // not in "no categories" anymore
+ // - dataChanged() is emitted
+ // - it's picked up in MainWindow::modlistSelectionsChanged() because the
+ // selected mods are gone
+ // - it calls setOverwriteMarkers(), which calls notifyChange() again and
+ // ends up here
+ // - dataChanged() is emitted again
+ //
+ // at this point, MO crashes because dataChanged() is not reentrant: it's in
+ // the middle of modifying internal data and crashes when trying to change an
+ // internal vector
+ //
+ // long story short, this prevents reentrancy
+ if (m_InNotifyChange) {
+ return;
+ }
+
+ m_InNotifyChange = true;
+ Guard g([&]{ m_InNotifyChange = false; });
+
if (rowStart < 0) {
m_Overwrite.clear();
m_Overwritten.clear();
@@ -1247,6 +1292,7 @@ void ModList::dropModeUpdate(bool dropOnItems)
QString ModList::getColumnName(int column)
{
switch (column) {
+ case COL_CONFLICTFLAGS: return tr("Conflicts");
case COL_FLAGS: return tr("Flags");
case COL_CONTENT: return tr("Content");
case COL_NAME: return tr("Mod Name");
@@ -1272,7 +1318,8 @@ QString ModList::getColumnToolTip(int column)
case COL_CATEGORY: return tr("Category of the mod.");
case COL_GAME: return tr("The source game which was the origin of this mod.");
case COL_MODID: return tr("Id of the mod as used on Nexus.");
- case COL_FLAGS: return tr("Emblemes to highlight things that might require attention.");
+ case COL_CONFLICTFLAGS: return tr("Indicators of file conflicts between mods.");
+ case COL_FLAGS: return tr("Emblems to highlight things that might require attention.");
case COL_CONTENT: return tr("Depicts the content of the mod:<br>"
"<table cellspacing=7>"
"<tr><td><img src=\":/MO/gui/content/plugin\" width=32/></td><td>Game plugins (esp/esm/esl)</td></tr>"
@@ -1324,7 +1371,7 @@ bool ModList::moveSelection(QAbstractItemView *itemView, int direction)
QModelIndexList rows = selectionModel->selectedRows();
if (direction > 0) {
for (int i = 0; i < rows.size() / 2; ++i) {
- rows.swap(i, rows.size() - i - 1);
+ rows.swapItemsAt(i, rows.size() - i - 1);
}
}
for (QModelIndex idx : rows) {
diff --git a/src/modlist.h b/src/modlist.h
index 5ce32f6e..3626c50c 100644
--- a/src/modlist.h
+++ b/src/modlist.h
@@ -57,6 +57,7 @@ public:
enum EColumn {
COL_NAME,
+ COL_CONFLICTFLAGS,
COL_FLAGS,
COL_CONTENT,
COL_CATEGORY,
@@ -66,8 +67,7 @@ public:
COL_INSTALLTIME,
COL_PRIORITY,
COL_NOTES,
-
- COL_LASTCOLUMN = COL_NOTES
+ COL_LASTCOLUMN = COL_NOTES,
};
typedef boost::signals2::signal<void (const QString &, ModStates)> SignalModStateChanged;
@@ -289,6 +289,8 @@ private:
QString getFlagText(ModInfo::EFlag flag, ModInfo::Ptr modInfo) const;
+ QString getConflictFlagText(ModInfo::EConflictFlag flag, ModInfo::Ptr modInfo) const;
+
static QString getColumnToolTip(int column);
QVariantList contentsToIcons(const std::vector<ModInfo::EContent> &content) const;
@@ -341,6 +343,7 @@ private:
std::set<int> m_RequestIDs;
mutable bool m_Modified;
+ bool m_InNotifyChange;
QFontMetrics m_FontMetrics;
diff --git a/src/modlistsortproxy.cpp b/src/modlistsortproxy.cpp
index 2d9ea4a5..162b0653 100644
--- a/src/modlistsortproxy.cpp
+++ b/src/modlistsortproxy.cpp
@@ -22,6 +22,7 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#include "profile.h"
#include "messagedialog.h"
#include "qtgroupingproxy.h"
+#include <log.h>
#include <QMenu>
#include <QCheckBox>
#include <QWidgetAction>
@@ -30,14 +31,14 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#include <QDebug>
#include <QTreeView>
+using namespace MOBase;
ModListSortProxy::ModListSortProxy(Profile* profile, QObject *parent)
: QSortFilterProxyModel(parent)
, m_Profile(profile)
- , m_CategoryFilter()
- , m_CurrentFilter()
, m_FilterActive(false)
- , m_FilterMode(FILTER_AND)
+ , m_FilterMode(FilterAnd)
+ , m_FilterSeparators(SeparatorFilter)
{
setDynamicSortFilter(true); // this seems to work without dynamicsortfilter
// but I don't know why. This should be necessary
@@ -50,26 +51,20 @@ void ModListSortProxy::setProfile(Profile *profile)
void ModListSortProxy::updateFilterActive()
{
- m_FilterActive = ((m_CategoryFilter.size() > 0)
- || (m_ContentFilter.size() > 0)
- || !m_CurrentFilter.isEmpty());
+ m_FilterActive = (!m_Criteria.empty() || !m_Filter.isEmpty());
emit filterActive(m_FilterActive);
}
-void ModListSortProxy::setCategoryFilter(const std::vector<int> &categories)
+void ModListSortProxy::setCriteria(const std::vector<Criteria>& criteria)
{
- //avoid refreshing the filter unless we are checking all mods for update.
- if (categories != m_CategoryFilter || (!categories.empty() && categories.at(0) == CategoryFactory::CATEGORY_SPECIAL_UPDATEAVAILABLE)) {
- m_CategoryFilter = categories;
- updateFilterActive();
- invalidate();
- }
-}
+ // avoid refreshing the filter unless we are checking all mods for update.
+ const bool changed = (criteria != m_Criteria);
+ const bool isForUpdates = (
+ !criteria.empty() &&
+ criteria[0].id == CategoryFactory::UpdateAvailable);
-void ModListSortProxy::setContentFilter(const std::vector<int> &content)
-{
- if (content != m_ContentFilter) {
- m_ContentFilter = content;
+ if (changed || isForUpdates) {
+ m_Criteria = criteria;
updateFilterActive();
invalidate();
}
@@ -120,6 +115,17 @@ unsigned long ModListSortProxy::flagsId(const std::vector<ModInfo::EFlag> &flags
return result;
}
+unsigned long ModListSortProxy::conflictFlagsId(const std::vector<ModInfo::EConflictFlag>& flags) const
+{
+ unsigned long result = 0;
+ for (ModInfo::EConflictFlag flag : flags) {
+ if ((flag != ModInfo::FLAG_OVERWRITE_CONFLICT)) {
+ result += 1 << (int)flag;
+ }
+ }
+ return result;
+}
+
bool ModListSortProxy::lessThan(const QModelIndex &left,
const QModelIndex &right) const
{
@@ -160,6 +166,16 @@ bool ModListSortProxy::lessThan(const QModelIndex &left,
lt = flagsId(leftFlags) < flagsId(rightFlags);
}
} break;
+ case ModList::COL_CONFLICTFLAGS: {
+ std::vector<ModInfo::EConflictFlag> leftFlags = leftMod->getConflictFlags();
+ std::vector<ModInfo::EConflictFlag> rightFlags = rightMod->getConflictFlags();
+ if (leftFlags.size() != rightFlags.size()) {
+ lt = leftFlags.size() < rightFlags.size();
+ }
+ else {
+ lt = conflictFlagsId(leftFlags) < conflictFlagsId(rightFlags);
+ }
+ } break;
case ModList::COL_CONTENT: {
std::vector<ModInfo::EContent> lContent = leftMod->getContents();
std::vector<ModInfo::EContent> rContent = rightMod->getContents();
@@ -194,7 +210,7 @@ bool ModListSortProxy::lessThan(const QModelIndex &left,
QString rightCatName = categories.getCategoryName(categories.getCategoryIndex(rightMod->getPrimaryCategory()));
lt = leftCatName < rightCatName;
} catch (const std::exception &e) {
- qCritical("failed to compare categories: %s", e.what());
+ log::error("failed to compare categories: {}", e.what());
}
}
}
@@ -240,24 +256,22 @@ bool ModListSortProxy::lessThan(const QModelIndex &left,
// nop, already compared by priority
} break;
default: {
- qWarning() << "Sorting is not defined for column " << left.column();
+ log::warn("Sorting is not defined for column {}", left.column());
} break;
}
return lt;
}
-void ModListSortProxy::updateFilter(const QString &filter)
+void ModListSortProxy::updateFilter(const QString& filter)
{
- m_CurrentFilter = filter;
+ m_Filter = filter;
updateFilterActive();
- // using invalidateFilter here should be enough but that crashes the application? WTF?
- // invalidateFilter();
invalidate();
}
-bool ModListSortProxy::hasConflictFlag(const std::vector<ModInfo::EFlag> &flags) const
+bool ModListSortProxy::hasConflictFlag(const std::vector<ModInfo::EConflictFlag> &flags) const
{
- for (ModInfo::EFlag flag : flags) {
+ for (ModInfo::EConflictFlag flag : flags) {
if ((flag == ModInfo::FLAG_CONFLICT_MIXED) ||
(flag == ModInfo::FLAG_CONFLICT_OVERWRITE) ||
(flag == ModInfo::FLAG_CONFLICT_OVERWRITTEN) ||
@@ -276,99 +290,198 @@ bool ModListSortProxy::hasConflictFlag(const std::vector<ModInfo::EFlag> &flags)
bool ModListSortProxy::filterMatchesModAnd(ModInfo::Ptr info, bool enabled) const
{
- for (auto iter = m_CategoryFilter.begin(); iter != m_CategoryFilter.end(); ++iter) {
- switch (*iter) {
- case CategoryFactory::CATEGORY_SPECIAL_CHECKED: {
- if (!enabled && !info->alwaysEnabled() && !info->hasFlag(ModInfo::FLAG_SEPARATOR)) return false;
- } break;
- case CategoryFactory::CATEGORY_SPECIAL_UNCHECKED: {
- if (enabled || info->alwaysEnabled()) return false;
- } break;
- case CategoryFactory::CATEGORY_SPECIAL_UPDATEAVAILABLE: {
- if (!info->updateAvailable() && !info->downgradeAvailable()) return false;
- } break;
- case CategoryFactory::CATEGORY_SPECIAL_NOCATEGORY: {
- if (info->getCategories().size() > 0) return false;
- } break;
- case CategoryFactory::CATEGORY_SPECIAL_CONFLICT: {
- if (!hasConflictFlag(info->getFlags())) return false;
- } break;
- case CategoryFactory::CATEGORY_SPECIAL_NOTENDORSED: {
- ModInfo::EEndorsedState state = info->endorsedState();
- if (state != ModInfo::ENDORSED_FALSE) return false;
- } break;
- case CategoryFactory::CATEGORY_SPECIAL_BACKUP: {
- if (!info->hasFlag(ModInfo::FLAG_BACKUP)) return false;
- } break;
- case CategoryFactory::CATEGORY_SPECIAL_MANAGED: {
- if (info->hasFlag(ModInfo::FLAG_FOREIGN)) return false;
- } break;
- case CategoryFactory::CATEGORY_SPECIAL_UNMANAGED: {
- if (!info->hasFlag(ModInfo::FLAG_FOREIGN)) return false;
- } break;
- default: {
- if (!info->categorySet(*iter)) return false;
- } break;
+ for (auto&& c : m_Criteria) {
+ if (!criteriaMatchMod(info, enabled, c)) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+bool ModListSortProxy::filterMatchesModOr(ModInfo::Ptr info, bool enabled) const
+{
+ for (auto&& c : m_Criteria) {
+ if (criteriaMatchMod(info, enabled, c)) {
+ return true;
}
}
- foreach (int content, m_ContentFilter) {
- if (!info->hasContent(static_cast<ModInfo::EContent>(content))) return false;
+ if (!m_Criteria.empty()) {
+ // nothing matched
+ return false;
}
return true;
}
-bool ModListSortProxy::filterMatchesModOr(ModInfo::Ptr info, bool enabled) const
+bool ModListSortProxy::optionsMatchMod(ModInfo::Ptr info, bool) const
{
- for (auto iter = m_CategoryFilter.begin(); iter != m_CategoryFilter.end(); ++iter) {
- switch (*iter) {
- case CategoryFactory::CATEGORY_SPECIAL_CHECKED: {
- if (enabled || info->alwaysEnabled()) return true;
- } break;
- case CategoryFactory::CATEGORY_SPECIAL_UNCHECKED: {
- if (!enabled && !info->alwaysEnabled()) return true;
- } break;
- case CategoryFactory::CATEGORY_SPECIAL_UPDATEAVAILABLE: {
- if (info->updateAvailable() || info->downgradeAvailable()) return true;
- } break;
- case CategoryFactory::CATEGORY_SPECIAL_NOCATEGORY: {
- if (info->getCategories().size() == 0) return true;
- } break;
- case CategoryFactory::CATEGORY_SPECIAL_CONFLICT: {
- if (hasConflictFlag(info->getFlags())) return true;
- } break;
- case CategoryFactory::CATEGORY_SPECIAL_NOTENDORSED: {
- ModInfo::EEndorsedState state = info->endorsedState();
- if ((state == ModInfo::ENDORSED_FALSE) || (state == ModInfo::ENDORSED_NEVER)) return true;
- } break;
- case CategoryFactory::CATEGORY_SPECIAL_BACKUP: {
- if (info->hasFlag(ModInfo::FLAG_BACKUP)) return true;
- } break;
- case CategoryFactory::CATEGORY_SPECIAL_MANAGED: {
- if (!info->hasFlag(ModInfo::FLAG_FOREIGN)) return true;
- } break;
- case CategoryFactory::CATEGORY_SPECIAL_UNMANAGED: {
- if (info->hasFlag(ModInfo::FLAG_FOREIGN)) return true;
- } break;
- default: {
- if (info->categorySet(*iter)) return true;
- } break;
+
+ return true;
+}
+
+bool ModListSortProxy::criteriaMatchMod(
+ ModInfo::Ptr info, bool enabled, const Criteria& c) const
+{
+ bool b = false;
+
+ switch (c.type)
+ {
+ case TypeSpecial: // fall-through
+ case TypeCategory:
+ {
+ b = categoryMatchesMod(info, enabled, c.id);
+ break;
+ }
+
+ case TypeContent:
+ {
+ b = contentMatchesMod(info, enabled, c.id);
+ break;
+ }
+
+ default:
+ {
+ log::error("bad criteria type {}", c.type);
+ break;
}
}
- foreach (int content, m_ContentFilter) {
- if (info->hasContent(static_cast<ModInfo::EContent>(content))) return true;
+ if (c.inverse) {
+ b = !b;
}
- return false;
+ return b;
+}
+
+bool ModListSortProxy::categoryMatchesMod(
+ ModInfo::Ptr info, bool enabled, int category) const
+{
+ bool b = false;
+
+ switch (category)
+ {
+ case CategoryFactory::Checked:
+ {
+ b = (enabled || info->alwaysEnabled());
+ break;
+ }
+
+ case CategoryFactory::UpdateAvailable:
+ {
+ b = (info->updateAvailable() || info->downgradeAvailable());
+ break;
+ }
+
+ case CategoryFactory::HasCategory:
+ {
+ b = !info->getCategories().empty();
+ break;
+ }
+
+ case CategoryFactory::Conflict:
+ {
+ b = (hasConflictFlag(info->getConflictFlags()));
+ break;
+ }
+
+ case CategoryFactory::Endorsed:
+ {
+ b = (info->endorsedState() == ModInfo::ENDORSED_TRUE);
+ break;
+ }
+
+ case CategoryFactory::Backup:
+ {
+ b = (info->hasFlag(ModInfo::FLAG_BACKUP));
+ break;
+ }
+
+ case CategoryFactory::Managed:
+ {
+ b = (!info->hasFlag(ModInfo::FLAG_FOREIGN));
+ break;
+ }
+
+ case CategoryFactory::HasGameData:
+ {
+ b = !info->hasFlag(ModInfo::FLAG_INVALID);
+ break;
+ }
+
+ case CategoryFactory::HasNexusID:
+ {
+ // never show these
+ if (
+ info->hasFlag(ModInfo::FLAG_FOREIGN) ||
+ info->hasFlag(ModInfo::FLAG_BACKUP) ||
+ info->hasFlag(ModInfo::FLAG_OVERWRITE))
+ {
+ return false;
+ }
+
+ b = (info->getNexusID() > 0);
+ break;
+ }
+
+ case CategoryFactory::Tracked:
+ {
+ b = (info->trackedState() == ModInfo::TRACKED_TRUE);
+ break;
+ }
+
+ default:
+ {
+ b = (info->categorySet(category));
+ break;
+ }
+ }
+
+ return b;
+}
+
+bool ModListSortProxy::contentMatchesMod(ModInfo::Ptr info, bool enabled, int content) const
+{
+ return info->hasContent(static_cast<ModInfo::EContent>(content));
}
bool ModListSortProxy::filterMatchesMod(ModInfo::Ptr info, bool enabled) const
{
- if (!m_CurrentFilter.isEmpty()) {
+ // don't check if there are no filters selected
+ if (!m_FilterActive) {
+ return true;
+ }
+
+
+ // special case for separators
+ if (info->hasFlag(ModInfo::FLAG_SEPARATOR)) {
+ switch (m_FilterSeparators)
+ {
+ case SeparatorFilter:
+ {
+ // filter normally
+ break;
+ }
+
+ case SeparatorShow:
+ {
+ // force visible
+ return true;
+ }
+
+ case SeparatorHide:
+ {
+ // force hide
+ return false;
+ }
+ }
+ }
+
+
+ if (!m_Filter.isEmpty()) {
bool display = false;
- QString filterCopy = QString(m_CurrentFilter);
+ QString filterCopy = QString(m_Filter);
filterCopy.replace("||", ";").replace("OR", ";").replace("|", ";");
QStringList ORList = filterCopy.split(";", QString::SkipEmptyParts);
@@ -446,7 +559,8 @@ bool ModListSortProxy::filterMatchesMod(ModInfo::Ptr info, bool enabled) const
}
}//if (!m_CurrentFilter.isEmpty())
- if (m_FilterMode == FILTER_AND) {
+
+ if (m_FilterMode == FilterAnd) {
return filterMatchesModAnd(info, enabled);
}
else {
@@ -459,10 +573,12 @@ void ModListSortProxy::setColumnVisible(int column, bool visible)
m_EnabledColumns[column] = visible;
}
-void ModListSortProxy::setFilterMode(ModListSortProxy::FilterMode mode)
+void ModListSortProxy::setOptions(
+ ModListSortProxy::FilterMode mode, SeparatorsMode separators)
{
- if (m_FilterMode != mode) {
+ if (m_FilterMode != mode || separators != m_FilterSeparators) {
m_FilterMode = mode;
+ m_FilterSeparators = separators;
this->invalidate();
}
}
@@ -474,13 +590,13 @@ bool ModListSortProxy::filterAcceptsRow(int row, const QModelIndex &parent) cons
}
if (row >= static_cast<int>(m_Profile->numMods())) {
- qWarning("invalid row index: %d", row);
+ log::warn("invalid row index: {}", row);
return false;
}
QModelIndex idx = sourceModel()->index(row, 0, parent);
if (!idx.isValid()) {
- qDebug("invalid mod index");
+ log::debug("invalid mod index");
return false;
}
if (sourceModel()->hasChildren(idx)) {
@@ -539,8 +655,8 @@ void ModListSortProxy::aboutToChangeData()
// (at least with some Qt versions)
// this may be related to the fact that the item being edited may disappear from the view as a
// result of the edit
- m_PreChangeFilters = categoryFilter();
- setCategoryFilter(std::vector<int>());
+ m_PreChangeCriteria = m_Criteria;
+ setCriteria({});
}
void ModListSortProxy::postDataChanged()
@@ -549,8 +665,8 @@ void ModListSortProxy::postDataChanged()
// or at least the view continues to think it's being edited. As a result no new editor can be
// opened
QTimer::singleShot(10, [this] () {
- setCategoryFilter(m_PreChangeFilters);
- m_PreChangeFilters.clear();
+ setCriteria(m_PreChangeCriteria);
+ m_PreChangeCriteria.clear();
});
}
diff --git a/src/modlistsortproxy.h b/src/modlistsortproxy.h
index 5fe8d9d6..3a29b7f7 100644
--- a/src/modlistsortproxy.h
+++ b/src/modlistsortproxy.h
@@ -31,16 +31,43 @@ class ModListSortProxy : public QSortFilterProxyModel
Q_OBJECT
public:
+ enum FilterMode
+ {
+ FilterAnd,
+ FilterOr
+ };
+
+ enum CriteriaType {
+ TypeSpecial,
+ TypeCategory,
+ TypeContent
+ };
- enum FilterMode {
- FILTER_AND,
- FILTER_OR
+ enum SeparatorsMode
+ {
+ SeparatorFilter,
+ SeparatorShow,
+ SeparatorHide
};
- enum FilterType {
- TYPE_SPECIAL,
- TYPE_CATEGORY,
- TYPE_CONTENT
+ struct Criteria
+ {
+ CriteriaType type;
+ int id;
+ bool inverse;
+
+ bool operator==(const Criteria& other) const
+ {
+ return
+ (type == other.type) &&
+ (id == other.id) &&
+ (inverse == other.inverse);
+ }
+
+ bool operator!=(const Criteria& other) const
+ {
+ return !(*this == other);
+ }
};
public:
@@ -49,10 +76,6 @@ public:
void setProfile(Profile *profile);
- void setCategoryFilter(const std::vector<int> &categories);
- std::vector<int> categoryFilter() const { return m_CategoryFilter; }
-
- void setContentFilter(const std::vector<int> &content);
virtual Qt::ItemFlags flags(const QModelIndex &modelIndex) const;
virtual bool dropMimeData(const QMimeData *data, Qt::DropAction action,
@@ -84,7 +107,8 @@ public:
*/
bool isFilterActive() const { return m_FilterActive; }
- void setFilterMode(FilterMode mode);
+ void setCriteria(const std::vector<Criteria>& criteria);
+ void setOptions(FilterMode mode, SeparatorsMode separators);
/**
* @brief tests if the specified index has child nodes
@@ -118,7 +142,8 @@ protected:
private:
unsigned long flagsId(const std::vector<ModInfo::EFlag> &flags) const;
- bool hasConflictFlag(const std::vector<ModInfo::EFlag> &flags) const;
+ unsigned long conflictFlagsId(const std::vector<ModInfo::EConflictFlag>& flags) const;
+ bool hasConflictFlag(const std::vector<ModInfo::EConflictFlag> &flags) const;
void updateFilterActive();
bool filterMatchesModAnd(ModInfo::Ptr info, bool enabled) const;
bool filterMatchesModOr(ModInfo::Ptr info, bool enabled) const;
@@ -129,19 +154,21 @@ private slots:
void postDataChanged();
private:
-
- Profile *m_Profile;
-
- std::vector<int> m_CategoryFilter;
- std::vector<int> m_ContentFilter;
+ Profile* m_Profile;
+ std::vector<Criteria> m_Criteria;
+ QString m_Filter;
std::bitset<ModList::COL_LASTCOLUMN + 1> m_EnabledColumns;
- QString m_CurrentFilter;
bool m_FilterActive;
FilterMode m_FilterMode;
+ SeparatorsMode m_FilterSeparators;
- std::vector<int> m_PreChangeFilters;
+ std::vector<Criteria> m_PreChangeCriteria;
+ bool optionsMatchMod(ModInfo::Ptr info, bool enabled) const;
+ bool criteriaMatchMod(ModInfo::Ptr info, bool enabled, const Criteria& c) const;
+ bool categoryMatchesMod(ModInfo::Ptr info, bool enabled, int category) const;
+ bool contentMatchesMod(ModInfo::Ptr info, bool enabled, int content) const;
};
#endif // MODLISTSORTPROXY_H
diff --git a/src/motddialog.cpp b/src/motddialog.cpp
index ca1e60ad..eee80205 100644
--- a/src/motddialog.cpp
+++ b/src/motddialog.cpp
@@ -47,5 +47,5 @@ void MotDDialog::on_okButton_clicked()
void MotDDialog::linkClicked(const QUrl &url)
{
- shell::OpenLink(url);
+ shell::Open(url);
}
diff --git a/src/nexusinterface.cpp b/src/nexusinterface.cpp
index ee9acf2c..12310d4b 100644
--- a/src/nexusinterface.cpp
+++ b/src/nexusinterface.cpp
@@ -25,6 +25,7 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#include "bbcode.h"
#include <utility.h>
#include <util.h>
+#include <log.h>
#include <QApplication>
#include <QNetworkCookieJar>
@@ -40,12 +41,11 @@ using namespace MOShared;
void throttledWarning(const APIUserAccount& user)
{
- qCritical() <<
- QString(
- "You have fewer than %1 requests remaining (%2). Only downloads and "
- "login validation are being allowed.")
- .arg(APIUserAccount::ThrottleThreshold)
- .arg(user.remainingRequests());
+ log::error(
+ "You have fewer than {} requests remaining ({}). Only downloads and "
+ "login validation are being allowed.",
+ APIUserAccount::ThrottleThreshold,
+ user.remainingRequests());
}
@@ -208,12 +208,27 @@ APILimits NexusInterface::defaultAPILimits()
APILimits NexusInterface::parseLimits(const QNetworkReply* reply)
{
+ return parseLimits(reply->rawHeaderPairs());
+}
+
+APILimits NexusInterface::parseLimits(
+ const QList<QNetworkReply::RawHeaderPair>& headers)
+{
APILimits limits;
- limits.maxDailyRequests = reply->rawHeader("x-rl-daily-limit").toInt();
- limits.remainingDailyRequests = reply->rawHeader("x-rl-daily-remaining").toInt();
- limits.maxHourlyRequests = reply->rawHeader("x-rl-hourly-limit").toInt();
- limits.remainingHourlyRequests = reply->rawHeader("x-rl-hourly-remaining").toInt();
+ for (const auto& pair : headers) {
+ const auto name = QString(pair.first).toLower();
+
+ if (name == "x-rl-daily-limit") {
+ limits.maxDailyRequests = pair.second.toInt();
+ } else if (name == "x-rl-daily-remaining") {
+ limits.remainingDailyRequests = pair.second.toInt();
+ } else if (name == "x-rl-hourly-limit") {
+ limits.maxHourlyRequests = pair.second.toInt();
+ } else if (name == "x-rl-hourly-remaining") {
+ limits.remainingHourlyRequests = pair.second.toInt();
+ }
+ }
return limits;
}
@@ -265,52 +280,60 @@ void NexusInterface::setUserAccount(const APIUserAccount& user)
void NexusInterface::interpretNexusFileName(const QString &fileName, QString &modName, int &modID, bool query)
{
- //Look for something along the lines of modulename-Vn-m + any old rubbish.
- static std::regex exp(R"exp(^([a-zA-Z0-9_'"\-.() ]*?)([-_ ][VvRr]?[0-9_]+)?-([1-9][0-9]*).*\.(zip|rar|7z))exp");
- static std::regex simpleexp("^([a-zA-Z0-9_]+)");
+ // guess the mod name from the file name
+ static const QRegularExpression complex("^([a-zA-Z0-9_'\"\\-.() ]*?)([-_ ][VvRr]?[0-9_]+)?-([1-9][0-9]*).*\\.(zip|rar|7z)");
+ static const QRegularExpression simple("^[a-zA-Z0-9_]+");
+ auto complexMatch = complex.match(fileName);
+ auto simpleMatch = simple.match(fileName);
+ if (complexMatch.hasMatch()) {
+ modName = complexMatch.captured(1);
+ }
+ else if (simpleMatch.hasMatch()) {
+ modName = simpleMatch.captured(0);
+ }
+ else {
+ modName.clear();
+ }
- QByteArray fileNameUTF8 = fileName.toUtf8();
- std::cmatch result;
- if (std::regex_search(fileNameUTF8.constData(), result, exp)) {
- modName = QString::fromUtf8(result[1].str().c_str());
- modName = modName.replace('_', ' ').trimmed();
+ if (query) {
+ SelectionDialog selection(tr("Please pick the mod ID for \"%1\"").arg(fileName));
+ int index = 0;
+ auto splits = fileName.split(QRegExp("[^0-9]"), QString::KeepEmptyParts);
+ for (auto substr : splits) {
+ bool ok = false;
+ int value = substr.toInt(&ok);
+ if (ok) {
+ QString highlight(fileName);
+ highlight.insert(index, " *");
+ highlight.insert(index + substr.length() + 2, "* ");
- std::string candidate = result[3].str();
- std::string candidate2 = result[2].str();
- if (candidate2.length() != 0 && (candidate2.find_last_of("VvRr") == std::string::npos)) {
- // well, that second match might be an id too...
- size_t offset = strspn(candidate2.c_str(), "-_ ");
- if (offset < candidate2.length() && query) {
- SelectionDialog selection(tr("Failed to guess mod id for \"%1\", please pick the correct one").arg(fileName));
- QString r2Highlight(fileName);
- r2Highlight.insert(result.position(2) + result.length(2), "* ")
- .insert(result.position(2) + static_cast<int>(offset), " *");
- QString r3Highlight(fileName);
- r3Highlight.insert(result.position(3) + result.length(3), "* ").insert(result.position(3), " *");
+ QStringList choice;
+ choice << substr;
+ choice << (index > 0 ? fileName.left(index - 1) : substr);
+ selection.addChoice(substr, highlight, choice);
+ }
+ index += substr.length() + 1;
+ }
- selection.addChoice(candidate.c_str(), r3Highlight, static_cast<int>(strtol(candidate.c_str(), nullptr, 10)));
- selection.addChoice(candidate2.c_str() + offset, r2Highlight, static_cast<int>(abs(strtol(candidate2.c_str() + offset, nullptr, 10))));
- if (selection.exec() == QDialog::Accepted) {
- modID = selection.getChoiceData().toInt();
- } else {
- modID = -1;
- }
- } else {
+ if (selection.numChoices() > 0) {
+ if (selection.exec() == QDialog::Accepted) {
+ auto choice = selection.getChoiceData().toStringList();
+ modID = choice.at(0).toInt();
+ modName = choice.at(1);
+ modName = modName.replace('_', ' ').trimmed();
+ log::debug("user selected mod ID {} and mod name \"{}\"", modID, modName);
+ }
+ else {
+ log::debug("user canceled mod ID selection");
modID = -1;
}
- } else {
- modID = strtol(candidate.c_str(), nullptr, 10);
}
- qDebug("mod id guessed: %s -> %d", qUtf8Printable(fileName), modID);
- } else if (std::regex_search(fileNameUTF8.constData(), result, simpleexp)) {
- qDebug("simple expression matched, using name only");
- modName = QString::fromUtf8(result[1].str().c_str());
- modName = modName.replace('_', ' ').trimmed();
-
- modID = -1;
- } else {
- qDebug("no expression matched!");
- modName.clear();
+ else {
+ log::debug("no possible mod IDs found in file name");
+ modID = -1;
+ }
+ }
+ else {
modID = -1;
}
}
@@ -328,7 +351,7 @@ QString NexusInterface::getGameURL(QString gameName) const
if (game != nullptr) {
return "https://www.nexusmods.com/" + game->gameNexusName().toLower();
} else {
- qCritical("getGameURL can't find plugin for %s", qUtf8Printable(gameName));
+ log::error("getGameURL can't find plugin for {}", gameName);
return "";
}
}
@@ -339,7 +362,7 @@ QString NexusInterface::getOldModsURL(QString gameName) const
if (game != nullptr) {
return "https://" + game->gameNexusName().toLower() + ".nexusmods.com/mods";
} else {
- qCritical("getOldModsURL can't find plugin for %s", qUtf8Printable(gameName));
+ log::error("getOldModsURL can't find plugin for {}", gameName);
return "";
}
}
@@ -448,7 +471,7 @@ int NexusInterface::requestUpdates(const int &modID, QObject *receiver, QVariant
IPluginGame *game = getGame(gameName);
if (game == nullptr) {
- qCritical("requestUpdates can't find plugin for %s", qUtf8Printable(gameName));
+ log::error("requestUpdates can't find plugin for {}", gameName);
return -1;
}
@@ -505,7 +528,7 @@ int NexusInterface::requestFileInfo(QString gameName, int modID, int fileID, QOb
{
IPluginGame *gamePlugin = getGame(gameName);
if (gamePlugin == nullptr) {
- qCritical("requestFileInfo can't find plugin for %s", qUtf8Printable(gameName));
+ log::error("requestFileInfo can't find plugin for {}", gameName);
return -1;
}
@@ -671,7 +694,7 @@ void NexusInterface::nextRequest()
} else if (getAccessManager()->validateWaiting()) {
return;
} else {
- qCritical() << tr("You must authorize MO2 in Settings -> Nexus to use the Nexus API.");
+ log::error("{}", tr("You must authorize MO2 in Settings -> Nexus to use the Nexus API."));
}
}
@@ -680,9 +703,13 @@ void NexusInterface::nextRequest()
QTime time = QTime::currentTime();
QTime targetTime;
targetTime.setHMS((time.hour() + 1) % 23, 5, 0);
- QString warning = tr("You've exceeded the Nexus API rate limit and requests are now being throttled. "
- "Your next batch of requests will be available in approximately %1 minutes and %2 seconds.");
- qWarning() << warning.arg(time.secsTo(targetTime) / 60).arg(time.secsTo(targetTime) % 60);
+ QString warning = tr(
+ "You've exceeded the Nexus API rate limit and requests are now being throttled. "
+ "Your next batch of requests will be available in approximately %1 minutes and %2 seconds.")
+ .arg(time.secsTo(targetTime) / 60)
+ .arg(time.secsTo(targetTime) % 60);
+
+ log::warn("{}", warning);
return;
}
@@ -732,8 +759,8 @@ void NexusInterface::nextRequest()
url = QString("%1/games/%2/mods/%3/files/%4/download_link?key=%5&expires=%6")
.arg(info.m_URL).arg(info.m_GameName).arg(info.m_ModID).arg(info.m_FileID).arg(fileInfo->nexusKey).arg(fileInfo->nexusExpires);
} else {
- qWarning() << tr("Aborting download: Either you clicked on a premium-only link and your account is not premium, "
- "or the download link was generated by a different account than the one stored in Mod Organizer.");
+ log::warn("{}", tr("Aborting download: Either you clicked on a premium-only link and your account is not premium, "
+ "or the download link was generated by a different account than the one stored in Mod Organizer."));
return;
}
} break;
@@ -765,7 +792,7 @@ void NexusInterface::nextRequest()
QNetworkRequest request(url);
request.setAttribute(QNetworkRequest::CacheSaveControlAttribute, false);
request.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::AlwaysNetwork);
- request.setRawHeader("APIKEY", m_AccessManager->apiKey().toUtf8());
+ request.setRawHeader("APIKEY", m_User.apiKey().toUtf8());
request.setHeader(QNetworkRequest::KnownHeaders::UserAgentHeader, m_AccessManager->userAgent(info.m_SubModule));
request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json");
request.setRawHeader("Protocol-Version", "1.0.0");
@@ -813,16 +840,16 @@ void NexusInterface::requestFinished(std::list<NXMRequestInfo>::iterator iter)
m_User.limits(parseLimits(reply));
if (!m_User.exhausted()) {
- qWarning("You appear to be making requests to the Nexus API too quickly and are being throttled. Please inform the MO2 team.");
+ log::warn("You appear to be making requests to the Nexus API too quickly and are being throttled. Please inform the MO2 team.");
}
else {
- qWarning("All API requests have been consumed and are now being denied.");
+ log::warn("All API requests have been consumed and are now being denied.");
}
emit requestsChanged(getAPIStats(), m_User);
- qWarning("Error: %s", reply->errorString().toUtf8().constData());
+ log::warn("Error: {}", reply->errorString());
} else {
- qWarning("request failed: %s", reply->errorString().toUtf8().constData());
+ log::warn("request failed: {}", reply->errorString());
}
emit nxmRequestFailed(iter->m_GameName, iter->m_ModID, iter->m_FileID, iter->m_UserData, iter->m_ID, reply->error(), reply->errorString());
} else {
@@ -841,7 +868,7 @@ void NexusInterface::requestFinished(std::list<NXMRequestInfo>::iterator iter)
if (nexusError.length() == 0) {
nexusError = tr("empty response");
}
- qDebug("nexus error: %s", qUtf8Printable(nexusError));
+ log::debug("nexus error: {}", nexusError);
emit nxmRequestFailed(iter->m_GameName, iter->m_ModID, iter->m_FileID, iter->m_UserData, iter->m_ID, reply->error(), nexusError);
} else {
QJsonDocument responseDoc = QJsonDocument::fromJson(data);
@@ -925,14 +952,13 @@ void NexusInterface::requestError(QNetworkReply::NetworkError)
{
QNetworkReply *reply = qobject_cast<QNetworkReply*>(sender());
if (reply == nullptr) {
- qWarning("invalid sender type");
+ log::warn("invalid sender type");
return;
}
- qCritical("request (%s) error: %s (%d)",
- qUtf8Printable(reply->url().toString()),
- qUtf8Printable(reply->errorString()),
- reply->error());
+ log::error(
+ "request ({}) error: {} ({})",
+ reply->url().toString(), reply->errorString(), reply->error());
}
@@ -940,7 +966,7 @@ void NexusInterface::requestTimeout()
{
QTimer *timer = qobject_cast<QTimer*>(sender());
if (timer == nullptr) {
- qWarning("invalid sender type");
+ log::warn("invalid sender type");
return;
}
for (std::list<NXMRequestInfo>::iterator iter = m_ActiveRequest.begin(); iter != m_ActiveRequest.end(); ++iter) {
diff --git a/src/nexusinterface.h b/src/nexusinterface.h
index 6e768149..0b1763c4 100644
--- a/src/nexusinterface.h
+++ b/src/nexusinterface.h
@@ -151,6 +151,7 @@ public:
public:
static APILimits defaultAPILimits();
static APILimits parseLimits(const QNetworkReply* reply);
+ static APILimits parseLimits(const QList<QNetworkReply::RawHeaderPair>& headers);
~NexusInterface();
diff --git a/src/nxmaccessmanager.cpp b/src/nxmaccessmanager.cpp
index ee6c03f1..20540593 100644
--- a/src/nxmaccessmanager.cpp
+++ b/src/nxmaccessmanager.cpp
@@ -18,6 +18,7 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
*/
#include "nxmaccessmanager.h"
+#include "ui_validationprogressdialog.h"
#include "iplugingame.h"
#include "nexusinterface.h"
#include "nxmurl.h"
@@ -40,21 +41,734 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#include <QJsonArray>
using namespace MOBase;
+using namespace std::chrono_literals;
-namespace {
- QString const nexusBaseUrl("https://api.nexusmods.com/v1");
+const QString NexusBaseUrl("https://api.nexusmods.com/v1");
+const QString NexusSSO("wss://sso.nexusmods.com");
+const QString NexusSSOPage("https://www.nexusmods.com/sso?id=%1&application=modorganizer2");
+
+
+ValidationProgressDialog::ValidationProgressDialog(NexusKeyValidator& v)
+ : m_validator(v), m_updateTimer(nullptr), m_first(true)
+{
+ ui.reset(new Ui::ValidationProgressDialog);
+ ui->setupUi(this);
+
+ connect(ui->hide, &QPushButton::clicked, [&]{ onHide(); });
+ connect(ui->cancel, &QPushButton::clicked, [&]{ onCancel(); });
+}
+
+void ValidationProgressDialog::setParentWidget(QWidget* w)
+{
+ const auto wasVisible = isVisible();
+
+ hide();
+ setParent(w, windowFlags() | Qt::Dialog);
+ setModal(false);
+
+ if (w && wasVisible) {
+ setVisible(true);
+ raise();
+ }
+}
+
+void ValidationProgressDialog::start()
+{
+ if (!m_updateTimer) {
+ m_updateTimer = new QTimer(this);
+ connect(m_updateTimer, &QTimer::timeout, [&]{ onTimer(); });
+ m_updateTimer->setInterval(100ms);
+ }
+
+ updateProgress();
+ m_updateTimer->start();
+
+ show();
+}
+
+void ValidationProgressDialog::stop()
+{
+ if (m_updateTimer) {
+ m_updateTimer->stop();
+ }
+
+ hide();
+}
+
+void ValidationProgressDialog::showEvent(QShowEvent* e)
+{
+ if (m_first) {
+ Settings::instance().geometry().centerOnMainWindowMonitor(this);
+ m_first = false;
+ }
+}
+
+void ValidationProgressDialog::closeEvent(QCloseEvent* e)
+{
+ hide();
+ e->ignore();
+}
+
+void ValidationProgressDialog::onHide()
+{
+ hide();
+}
+
+void ValidationProgressDialog::onCancel()
+{
+ m_validator.cancel();
+}
+
+void ValidationProgressDialog::onTimer()
+{
+ updateProgress();
+}
+
+void ValidationProgressDialog::updateProgress()
+{
+ const auto* current = m_validator.currentAttempt();
+
+ if (current) {
+ ui->progress->setRange(0, current->timeout().count());
+ ui->progress->setValue(current->elapsed().elapsed() / 1000);
+ } else {
+ // indeterminate
+ ui->progress->setRange(0, 0);
+ }
+
+ if (const auto* a=m_validator.lastAttempt()) {
+ ui->label->setText(a->message() + ". " + tr("Trying again..."));
+ } else if (current) {
+ ui->label->setText(tr("Connecting to Nexus..."));
+ } else {
+ ui->label->setText("?");
+ }
+}
+
+
+NexusSSOLogin::NexusSSOLogin()
+ : m_keyReceived(false), m_active(false)
+{
+ m_timeout.setInterval(10s);
+ m_timeout.setSingleShot(true);
+
+ QObject::connect(
+ &m_socket, &QWebSocket::connected,
+ [&]{ onConnected(); });
+
+ QObject::connect(
+ &m_socket, qOverload<QAbstractSocket::SocketError>(&QWebSocket::error),
+ [&](auto&& e){ onError(e); });
+
+ QObject::connect(
+ &m_socket, &QWebSocket::sslErrors,
+ [&](auto&& errors){ onSslErrors(errors); });
+
+ QObject::connect(
+ &m_socket, &QWebSocket::textMessageReceived,
+ [&](auto&& s){ onMessage(s); });
+
+ QObject::connect(
+ &m_socket, &QWebSocket::disconnected,
+ [&]{ onDisconnected(); });
+
+ QObject::connect(&m_timeout, &QTimer::timeout, [&]{ onTimeout(); });
+}
+
+QString NexusSSOLogin::stateToString(States s, const QString& e)
+{
+ switch (s)
+ {
+ case ConnectingToSSO:
+ return QObject::tr("Connecting to Nexus...");
+
+ case WaitingForToken:
+ return QObject::tr("Waiting for Nexus...");
+
+ case WaitingForBrowser:
+ return
+ QObject::tr("Opened Nexus in browser.") + "\n" +
+ QObject::tr("Switch to your browser and accept the request.");
+
+ case Finished:
+ return QObject::tr("Finished.");
+
+ case Timeout:
+ return
+ QObject::tr("No answer from Nexus.") + "\n" +
+ QObject::tr("A firewall might be blocking Mod Organizer.");
+
+ case ClosedByRemote:
+ return
+ QObject::tr("Nexus closed the connection.") + "\n" +
+ QObject::tr("A firewall might be blocking Mod Organizer.");
+
+ case Cancelled:
+ return QObject::tr("Cancelled.");
+
+ case Error: // fall-through
+ default:
+ {
+ if (e.isEmpty()) {
+ return QString("%1").arg(s);
+ } else {
+ return e;
+ }
+ }
+ }
+}
+
+void NexusSSOLogin::start()
+{
+ m_active = true;
+ setState(ConnectingToSSO);
+ m_timeout.start();
+ m_socket.open(NexusSSO);
+}
+
+void NexusSSOLogin::cancel()
+{
+ if (m_active) {
+ abort();
+ setState(Cancelled);
+ }
+}
+
+void NexusSSOLogin::close()
+{
+ if (m_active) {
+ m_active = false;
+ m_timeout.stop();
+ m_socket.close();
+ }
+}
+
+void NexusSSOLogin::abort()
+{
+ m_active = false;
+ m_timeout.stop();
+ m_socket.abort();
+}
+
+bool NexusSSOLogin::isActive() const
+{
+ return m_active;
+}
+
+void NexusSSOLogin::setState(States s, const QString& error)
+{
+ if (stateChanged) {
+ stateChanged(s, error);
+ }
+}
+
+void NexusSSOLogin::onConnected()
+{
+ setState(WaitingForToken);
+
+ m_keyReceived = false;
+
+ boost::uuids::random_generator generator;
+ boost::uuids::uuid sessionId = generator();
+ m_guid = boost::uuids::to_string(sessionId).c_str();
+
+ QJsonObject data;
+ data.insert(QString("id"), QJsonValue(m_guid));
+ data.insert(QString("protocol"), 2);
+
+ const QString message = QJsonDocument(data).toJson();
+ m_socket.sendTextMessage(message);
+}
+
+void NexusSSOLogin::onMessage(const QString& s)
+{
+ const QJsonDocument doc = QJsonDocument::fromJson(s.toUtf8());
+ const QVariantMap root = doc.object().toVariantMap();
+
+ if (!root["success"].toBool()) {
+ close();
+
+ setState(Error, QString("There was a problem with SSO initialization: %1")
+ .arg(root["error"].toString()));
+
+ return;
+ }
+
+ const QVariantMap data = root["data"].toMap();
+
+ if (data.contains("connection_token")) {
+ // first answer
+
+ // open browser
+ const auto url = NexusSSOPage.arg(m_guid);
+ shell::Open(url);
+
+ m_timeout.stop();
+ setState(WaitingForBrowser);
+ } else {
+ // second answer
+ const auto key = data["api_key"].toString();
+ close();
+
+ if (keyChanged) {
+ keyChanged(key);
+ }
+
+ setState(Finished);
+ }
+}
+
+void NexusSSOLogin::onDisconnected()
+{
+ if (m_active) {
+ if (!m_keyReceived) {
+ close();
+ setState(ClosedByRemote);
+ } else {
+ m_active = false;
+ }
+ }
+}
+
+void NexusSSOLogin::onError(QAbstractSocket::SocketError e)
+{
+ if (m_active) {
+ close();
+ setState(Error, m_socket.errorString());
+ }
+}
+
+void NexusSSOLogin::onSslErrors(const QList<QSslError>& errors)
+{
+ if (m_active) {
+ for (const auto& e : errors) {
+ setState(Error, e.errorString());
+ }
+ }
+}
+
+void NexusSSOLogin::onTimeout()
+{
+ abort();
+ setState(Timeout);
+}
+
+
+ValidationAttempt::ValidationAttempt(std::chrono::seconds timeout)
+ : m_reply(nullptr), m_result(None)
+{
+ m_timeout.setSingleShot(true);
+ m_timeout.setInterval(timeout);
+
+ QObject::connect(&m_timeout, &QTimer::timeout, [&]{ onTimeout(); });
+}
+
+void ValidationAttempt::start(NXMAccessManager& m, const QString& key)
+{
+ if (!sendRequest(m, key)) {
+ return;
+ }
+
+ m_elapsed.start();
+ m_timeout.start();
+
+ log::debug(
+ "validator: attempt started with timeout of {} seconds", timeout().count());
+}
+
+bool ValidationAttempt::sendRequest(
+ NXMAccessManager& m, const QString& key)
+{
+ const QString requestUrl(NexusBaseUrl + "/users/validate");
+ QNetworkRequest request(requestUrl);
+
+ request.setRawHeader("APIKEY", key.toUtf8());
+ request.setHeader(QNetworkRequest::KnownHeaders::UserAgentHeader, m.userAgent().toUtf8());
+ request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json");
+ request.setRawHeader("Protocol-Version", "1.0.0");
+ request.setRawHeader("Application-Name", "MO2");
+ request.setRawHeader("Application-Version", m.MOVersion().toUtf8());
+
+ m_reply = m.get(request);
+
+ if (!m_reply) {
+ setFailure(SoftError, QObject::tr("Failed to request %1").arg(requestUrl));
+ return false;
+ }
+
+ QObject::connect(
+ m_reply, &QNetworkReply::finished,
+ [&]{ onFinished(); });
+
+ QObject::connect(
+ m_reply, &QNetworkReply::sslErrors,
+ [&](auto&& errors){ onSslErrors(errors); });
+
+ return true;
+}
+
+void ValidationAttempt::cancel()
+{
+ if (!m_reply || m_result != None) {
+ // not running
+ return;
+ }
+
+ setFailure(Cancelled, QObject::tr("Cancelled"));
+
+ if (m_reply) {
+ m_reply->abort();
+ }
+
+ cleanup();
+}
+
+bool ValidationAttempt::done() const
+{
+ return (m_result != None);
+}
+
+ValidationAttempt::Result ValidationAttempt::result() const
+{
+ return m_result;
+}
+
+const QString& ValidationAttempt::message() const
+{
+ return m_message;
+}
+
+std::chrono::seconds ValidationAttempt::timeout() const
+{
+ return std::chrono::duration_cast<std::chrono::seconds>(
+ m_timeout.intervalAsDuration());
+}
+
+QElapsedTimer ValidationAttempt::elapsed() const
+{
+ return m_elapsed;
+}
+
+void ValidationAttempt::onFinished()
+{
+ if (m_result == Cancelled) {
+ return;
+ }
+
+ log::debug("validator attempt: request has finished");
+
+ if (!m_reply) {
+ // shouldn't happen
+ log::error("validator attempt: reply is null");
+ setFailure(HardError, QObject::tr("Internal error"));
+ return;
+ }
+
+ const auto code = m_reply->attribute(
+ QNetworkRequest::HttpStatusCodeAttribute).toInt();
+
+ if (code == 0) {
+ // request wasn't even sent
+ log::error("validator attempt: code is 0");
+ setFailure(SoftError, m_reply->errorString());
+ return;
+ }
+
+ const auto doc = QJsonDocument::fromJson(m_reply->readAll());
+ const auto headers = m_reply->rawHeaderPairs();
+ const auto httpError = m_reply->errorString();
+
+ const QJsonObject data = doc.object();
+
+ if (code != 200) {
+ // http request failed
+
+ QString s = m_reply->errorString();
+
+ const auto nexusMessage = data.value("message").toString();
+ if (!nexusMessage.isEmpty()) {
+ if (!s.isEmpty()) {
+ s += ", ";
+ }
+
+ s += nexusMessage;
+ }
+
+ if (s.isEmpty()) {
+ s = QObject::tr("HTTP code %1").arg(code);
+ } else {
+ s += QString(" (%1)").arg(code);
+ }
+
+ setFailure(SoftError, s);
+ return;
+ }
+
+ if (doc.isNull()) {
+ setFailure(HardError, QObject::tr("Invalid JSON"));
+ return;
+ }
+
+ if (!data.contains("user_id")) {
+ setFailure(HardError, QObject::tr("Bad response"));
+ return;
+ }
+
+ const int id = data.value("user_id").toInt();
+ const QString key = data.value("key").toString();
+ const QString name = data.value("name").toString();
+ const bool premium = data.value("is_premium").toBool();
+
+ if (key.isEmpty()) {
+ setFailure(HardError, QObject::tr("API key is empty"));
+ return;
+ }
+
+ const auto user = APIUserAccount()
+ .apiKey(key)
+ .id(QString("%1").arg(id))
+ .name(name)
+ .type(premium ? APIUserAccountTypes::Premium : APIUserAccountTypes::Regular)
+ .limits(NexusInterface::parseLimits(headers));
+
+ setSuccess(user);
+}
+
+void ValidationAttempt::onSslErrors(const QList<QSslError>& errors)
+{
+ log::error("validator attempt: ssl errors");
+
+ for (auto& e : errors) {
+ log::error(" . {}", e.errorString());
+ }
+
+ setFailure(HardError, QObject::tr("SSL error"));
+}
+
+void ValidationAttempt::onTimeout()
+{
+ setFailure(SoftError, QObject::tr("Timed out"));
+}
+
+void ValidationAttempt::setFailure(Result r, const QString& error)
+{
+ if (r != Cancelled) {
+ // don't spam the log
+ log::error("validator attempt: {}", error);
+ }
+
+ cleanup();
+
+ m_result = r;
+ m_message = error;
+
+ if (failure) {
+ failure();
+ }
+}
+
+void ValidationAttempt::setSuccess(const APIUserAccount& user)
+{
+ log::debug("validator attempt successful");
+ cleanup();
+
+ m_result = Success;
+ m_message = "";
+
+ if (success) {
+ success(user);
+ }
+}
+
+void ValidationAttempt::cleanup()
+{
+ m_timeout.stop();
+
+ if (m_reply) {
+ m_reply->disconnect();
+ m_reply->deleteLater();
+ m_reply = nullptr;
+ }
+}
+
+
+NexusKeyValidator::NexusKeyValidator(NXMAccessManager& am)
+ : m_manager(am)
+{
+}
+
+NexusKeyValidator::~NexusKeyValidator()
+{
+ cancel();
+}
+
+void NexusKeyValidator::start(const QString& key, Behaviour b)
+{
+ if (isActive()) {
+ log::debug("validator: trying to start while ongoing; ignoring");
+ return;
+ }
+
+ m_key = key;
+
+ switch (b)
+ {
+ case OneShot:
+ {
+ createAttempts({10s});
+ break;
+ }
+
+ case Retry:
+ {
+ createAttempts({5s, 5s, 10s});
+ break;
+ }
+ }
+
+ nextTry();
+}
+
+void NexusKeyValidator::createAttempts(
+ const std::vector<std::chrono::seconds>& timeouts)
+{
+ m_attempts.clear();
+
+ for (auto&& t : timeouts) {
+ m_attempts.push_back(std::make_unique<ValidationAttempt>(t));
+ }
+}
+
+void NexusKeyValidator::cancel()
+{
+ log::debug("validator: cancelled");
+
+ for (auto&& a : m_attempts) {
+ a->cancel();
+ }
+}
+
+bool NexusKeyValidator::isActive() const
+{
+ for (auto&& a : m_attempts) {
+ if (!a->done()) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+const ValidationAttempt* NexusKeyValidator::lastAttempt() const
+{
+ const ValidationAttempt* last = nullptr;
+
+ for (auto&& a : m_attempts) {
+ if (a->done()) {
+ last = a.get();
+ } else {
+ break;
+ }
+ }
+
+ return last;
+}
+
+const ValidationAttempt* NexusKeyValidator::currentAttempt() const
+{
+ for (auto&& a : m_attempts) {
+ if (!a->done()) {
+ return a.get();
+ }
+ }
+
+ return nullptr;
}
+bool NexusKeyValidator::nextTry()
+{
+ for (auto&& a : m_attempts) {
+ if (!a->done()) {
+ a->success = [&](auto&& user){ onAttemptSuccess(*a, user); };
+ a->failure = [&]{ onAttemptFailure(*a); };
+
+ a->start(m_manager, m_key);
+ return true;
+ }
+ }
+
+ // no more
+ return false;
+}
+
+void NexusKeyValidator::onAttemptSuccess(
+ const ValidationAttempt& a, const APIUserAccount& u)
+{
+ if (attemptFinished) {
+ attemptFinished(a);
+ }
+
+ setFinished(ValidationAttempt::Success, "", u);
+}
+
+void NexusKeyValidator::onAttemptFailure(const ValidationAttempt& a)
+{
+ if (attemptFinished) {
+ attemptFinished(a);
+ }
+
+ switch (a.result())
+ {
+ case ValidationAttempt::SoftError:
+ {
+ if (!nextTry()) {
+ setFinished(a.result(), a.message(), {});
+ }
+
+ break;
+ }
+
+ case ValidationAttempt::HardError:
+ {
+ cancel();
+ setFinished(a.result(), a.message(), {});
+ break;
+ }
+
+ case ValidationAttempt::Cancelled:
+ {
+ setFinished(ValidationAttempt::Cancelled, QObject::tr("Cancelled"), {});
+ break;
+ }
+ }
+}
+
+void NexusKeyValidator::setFinished(
+ ValidationAttempt::Result r, const QString& message,
+ std::optional<APIUserAccount> user)
+{
+ if (finished) {
+ finished(r, message, user);
+ }
+}
+
+
NXMAccessManager::NXMAccessManager(QObject *parent, const QString &moVersion)
: QNetworkAccessManager(parent)
- , m_ValidateReply(nullptr)
, m_MOVersion(moVersion)
+ , m_validator(*this)
+ , m_validationState(NotChecked)
{
- m_ValidateTimeout.setSingleShot(true);
- m_ValidateTimeout.setInterval(30000);
- connect(&m_ValidateTimeout, SIGNAL(timeout()), this, SLOT(validateTimeout()));
- setCookieJar(new PersistentCookieJar(
- QDir::fromNativeSeparators(Settings::instance().getCacheDirectory() + "/nexus_cookies.dat")));
+ m_validator.finished = [&](auto&& r, auto&& m, auto&& u) {
+ onValidatorFinished(r, m, u);
+ };
+
+ m_validator.attemptFinished = [&](auto&& a) {
+ onValidatorAttemptFinished(a);
+ };
+
+ setCookieJar(new PersistentCookieJar(QDir::fromNativeSeparators(
+ Settings::instance().paths().cache() + "/nexus_cookies.dat")));
if (networkAccessible() == QNetworkAccessManager::UnknownAccessibility) {
// why is this necessary all of a sudden?
@@ -62,11 +776,15 @@ NXMAccessManager::NXMAccessManager(QObject *parent, const QString &moVersion)
}
}
-NXMAccessManager::~NXMAccessManager()
+void NXMAccessManager::setTopLevelWidget(QWidget* w)
{
- if (m_ValidateReply != nullptr) {
- m_ValidateReply->deleteLater();
- m_ValidateReply = nullptr;
+ if (w) {
+ if (m_ProgressDialog) {
+ m_ProgressDialog->setParentWidget(w);
+ }
+ } else {
+ m_ProgressDialog.reset();
+ m_validator.cancel();
}
}
@@ -90,14 +808,13 @@ QNetworkReply *NXMAccessManager::createRequest(
}
}
-
void NXMAccessManager::showCookies() const
{
- QUrl url(nexusBaseUrl + "/");
+ QUrl url(NexusBaseUrl + "/");
for (const QNetworkCookie &cookie : cookieJar()->cookiesForUrl(url)) {
- qDebug("%s - %s (expires: %s)",
+ log::debug("{} - {} (expires: {})",
cookie.name().constData(), cookie.value().constData(),
- qUtf8Printable(cookie.expirationDate().toString()));
+ cookie.expirationDate().toString());
}
}
@@ -107,94 +824,116 @@ void NXMAccessManager::clearCookies()
if (jar != nullptr) {
jar->clear();
} else {
- qWarning("failed to clear cookies, invalid cookie jar");
+ log::warn("failed to clear cookies, invalid cookie jar");
}
}
-void NXMAccessManager::startValidationCheck()
+void NXMAccessManager::startValidationCheck(const QString& key)
{
- qDebug("Checking Nexus API Key...");
- QString requestString = nexusBaseUrl + "/users/validate";
+ m_validationState = NotChecked;
+ m_validator.start(key, NexusKeyValidator::Retry);
- QNetworkRequest request(requestString);
- request.setRawHeader("APIKEY", m_ApiKey.toUtf8());
- request.setHeader(QNetworkRequest::KnownHeaders::UserAgentHeader, userAgent().toUtf8());
- request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json");
- request.setRawHeader("Protocol-Version", "1.0.0");
- request.setRawHeader("Application-Name", "MO2");
- request.setRawHeader("Application-Version", m_MOVersion.toUtf8());
+ if (m_ProgressDialog) {
+ // don't show the progress dialog on startup for the first attempt; the
+ // dialog will be shown in onValidatorAttemptFinished() if it failed
+ startProgress();
+ }
+}
- m_ProgressDialog = new QProgressDialog(nullptr);
- m_ProgressDialog->setLabelText(tr("Validating Nexus Connection"));
- QList<QPushButton*> buttons = m_ProgressDialog->findChildren<QPushButton*>();
- buttons.at(0)->setEnabled(false);
- m_ProgressDialog->show();
- QCoreApplication::processEvents(); // for some reason the whole app hangs during the login. This way the user has at least a little feedback
+void NXMAccessManager::onValidatorFinished(
+ ValidationAttempt::Result r, const QString& message,
+ std::optional<APIUserAccount> user)
+{
+ stopProgress();
- m_ValidateReply = get(request);
- m_ValidateTimeout.start();
- m_ValidateState = VALIDATE_CHECKING;
- connect(m_ValidateReply, SIGNAL(finished()), this, SLOT(validateFinished()));
- connect(m_ValidateReply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(validateError(QNetworkReply::NetworkError)));
+ if (user) {
+ m_validationState = Valid;
+ emit credentialsReceived(*user);
+ emit validateSuccessful(true);
+ } else {
+ if (r == ValidationAttempt::Cancelled) {
+ m_validationState = NotChecked;
+ } else {
+ m_validationState = Invalid;
+ emit validateFailed(message);
+ }
+ }
}
+void NXMAccessManager::onValidatorAttemptFinished(const ValidationAttempt& a)
+{
+ if (!m_ProgressDialog) {
+ switch (a.result())
+ {
+ case ValidationAttempt::SoftError:
+ case ValidationAttempt::HardError:
+ {
+ startProgress();
+ break;
+ }
+
+ case ValidationAttempt::None:
+ case ValidationAttempt::Success:
+ case ValidationAttempt::Cancelled:
+ default:
+ {
+ // don't show the dialog
+ break;
+ }
+ }
+ }
+}
bool NXMAccessManager::validated() const
{
- if (m_ValidateState == VALIDATE_CHECKING) {
- QProgressDialog progress;
- progress.setLabelText(tr("Validating Nexus Connection"));
- QList<QPushButton*> buttons = m_ProgressDialog->findChildren<QPushButton*>();
- buttons.at(0)->setEnabled(false);
- progress.show();
- while (m_ValidateState == VALIDATE_CHECKING) {
- QCoreApplication::processEvents();
- QThread::msleep(100);
- }
- progress.hide();
+ if (m_validationState == Valid) {
+ return true;
}
- return m_ValidateState == VALIDATE_VALID;
-}
+ if (m_validator.isActive()) {
+ const_cast<NXMAccessManager*>(this)->startProgress();
+ }
+ return false;
+}
void NXMAccessManager::refuseValidation()
{
- m_ValidateState = VALIDATE_REFUSED;
+ m_validationState = Invalid;
}
-
bool NXMAccessManager::validateAttempted() const
{
- return m_ValidateState != VALIDATE_NOT_CHECKED;
+ return (m_validationState != NotChecked);
}
-
bool NXMAccessManager::validateWaiting() const
{
- return m_ValidateReply != nullptr;
+ return m_validator.isActive();
}
-
void NXMAccessManager::apiCheck(const QString &apiKey, bool force)
{
- if (m_ValidateReply != nullptr) {
+ if (m_validator.isActive()) {
return;
}
if (force) {
- m_ValidateState = VALIDATE_NOT_CHECKED;
+ m_validationState = NotChecked;
}
- if (m_ValidateState == VALIDATE_VALID) {
+ if (m_validationState == Valid) {
emit validateSuccessful(false);
return;
}
- m_ApiKey = apiKey;
- startValidationCheck();
+ startValidationCheck(apiKey);
}
+const QString& NXMAccessManager::MOVersion() const
+{
+ return m_MOVersion;
+}
QString NXMAccessManager::userAgent(const QString &subModule) const
{
@@ -213,109 +952,24 @@ QString NXMAccessManager::userAgent(const QString &subModule) const
return QString("Mod Organizer/%1 (%2) Qt/%3").arg(m_MOVersion, comments.join("; "), qVersion());
}
-
-QString NXMAccessManager::apiKey() const
-{
- return m_ApiKey;
-}
-
void NXMAccessManager::clearApiKey()
{
- m_ApiKey = "";
- m_ValidateState = VALIDATE_NOT_VALID;
-
+ m_validator.cancel();
emit credentialsReceived(APIUserAccount());
}
-void NXMAccessManager::validateTimeout()
+void NXMAccessManager::startProgress()
{
- m_ValidateTimeout.stop();
- if (m_ProgressDialog != nullptr) {
- m_ProgressDialog->hide();
- m_ProgressDialog->deleteLater();
- m_ProgressDialog = nullptr;
+ if (!m_ProgressDialog) {
+ m_ProgressDialog.reset(new ValidationProgressDialog(m_validator));
}
- m_ApiKey.clear();
- m_ValidateState = VALIDATE_NOT_VALID;
- if (m_ValidateReply != nullptr) {
- m_ValidateReply->deleteLater();
- m_ValidateReply = nullptr;
- }
-
- emit validateFailed(tr("There was a timeout during the request"));
-}
-
-
-void NXMAccessManager::validateError(QNetworkReply::NetworkError)
-{
- m_ValidateTimeout.stop();
- if (m_ProgressDialog != nullptr) {
- m_ProgressDialog->hide();
- m_ProgressDialog->deleteLater();
- m_ProgressDialog = nullptr;
- }
- m_ApiKey.clear();
- m_ValidateState = VALIDATE_NOT_VALID;
-
- if (m_ValidateReply != nullptr) {
- m_ValidateReply->disconnect();
- QString error = m_ValidateReply->errorString();
- m_ValidateReply->deleteLater();
- m_ValidateReply = nullptr;
- emit validateFailed(error);
- } else {
- emit validateFailed(tr("Unknown error"));
- }
+ m_ProgressDialog->start();
}
-
-void NXMAccessManager::validateFinished()
+void NXMAccessManager::stopProgress()
{
- m_ValidateTimeout.stop();
- if (m_ProgressDialog != nullptr) {
- m_ProgressDialog->deleteLater();
- m_ProgressDialog = nullptr;
- }
-
- if (m_ValidateReply != nullptr) {
- QJsonDocument jdoc = QJsonDocument::fromJson(m_ValidateReply->readAll());
- if (!jdoc.isNull()) {
- QJsonObject credentialsData = jdoc.object();
- if (credentialsData.contains("user_id")) {
- int id = credentialsData.value("user_id").toInt();
- QString name = credentialsData.value("name").toString();
- bool premium = credentialsData.value("is_premium").toBool();
-
- const auto user = APIUserAccount()
- .id(QString("%1").arg(id))
- .name(name)
- .type(premium ? APIUserAccountTypes::Premium : APIUserAccountTypes::Regular)
- .limits(NexusInterface::parseLimits(m_ValidateReply));
-
-
- emit credentialsReceived(user);
-
- m_ValidateReply->deleteLater();
- m_ValidateReply = nullptr;
-
- m_ValidateState = VALIDATE_VALID;
- emit validateSuccessful(true);
-
- } else {
- m_ApiKey.clear();
- m_ValidateState = VALIDATE_NOT_VALID;
- emit validateFailed(tr("Validation failed, please reauthenticate in the Settings -> Nexus tab: %1").arg(credentialsData.value("message").toString()));
- }
- } else {
- m_ApiKey.clear();
- m_ValidateState = VALIDATE_NOT_CHECKED;
- emit validateFailed(tr("Could not parse response. Invalid JSON."));
- }
- }
- else {
- m_ApiKey.clear();
- m_ValidateState = VALIDATE_NOT_CHECKED;
- emit validateFailed(tr("Unknown error."));
+ if (m_ProgressDialog) {
+ m_ProgressDialog->stop();
}
}
diff --git a/src/nxmaccessmanager.h b/src/nxmaccessmanager.h
index 1bdeae40..6a45d880 100644
--- a/src/nxmaccessmanager.h
+++ b/src/nxmaccessmanager.h
@@ -25,9 +25,182 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#include <QTimer>
#include <QNetworkReply>
#include <QProgressDialog>
+#include <QElapsedTimer>
+#include <QDialogButtonBox>
+#include <QWebSocket>
#include <set>
namespace MOBase { class IPluginGame; }
+namespace Ui { class ValidationProgressDialog; }
+class NXMAccessManager;
+
+class NexusSSOLogin
+{
+public:
+ enum States
+ {
+ ConnectingToSSO,
+ WaitingForToken,
+ WaitingForBrowser,
+ Finished,
+ Timeout,
+ ClosedByRemote,
+ Cancelled,
+ Error
+ };
+
+ std::function<void (QString)> keyChanged;
+ std::function<void (States, QString)> stateChanged;
+
+ static QString stateToString(States s, const QString& e);
+
+ NexusSSOLogin();
+
+ void start();
+ void cancel();
+
+ bool isActive() const;
+
+private:
+ QWebSocket m_socket;
+ QString m_guid;
+ bool m_keyReceived;
+ bool m_active;
+ QTimer m_timeout;
+
+ void setState(States s, const QString& error={});
+
+ void close();
+ void abort();
+
+ void onConnected();
+ void onMessage(const QString& s);
+ void onDisconnected();
+ void onError(QAbstractSocket::SocketError e);
+ void onSslErrors(const QList<QSslError>& errors);
+ void onTimeout();
+};
+
+
+class ValidationAttempt
+{
+public:
+ enum Result
+ {
+ None,
+ Success,
+ SoftError,
+ HardError,
+ Cancelled
+ };
+
+ std::function<void (APIUserAccount)> success;
+ std::function<void ()> failure;
+
+ ValidationAttempt(std::chrono::seconds timeout);
+ ValidationAttempt(const ValidationAttempt&) = delete;
+ ValidationAttempt& operator=(const ValidationAttempt&) = delete;
+
+ void start(NXMAccessManager& m, const QString& key);
+ void cancel();
+
+ bool done() const;
+ Result result() const;
+ const QString& message() const;
+ std::chrono::seconds timeout() const;
+ QElapsedTimer elapsed() const;
+
+private:
+ QNetworkReply* m_reply;
+ Result m_result;
+ QString m_message;
+ QTimer m_timeout;
+ QElapsedTimer m_elapsed;
+
+ bool sendRequest(NXMAccessManager& m, const QString& key);
+
+ void onFinished();
+ void onSslErrors(const QList<QSslError>& errors);
+ void onTimeout();
+
+ void setFailure(Result r, const QString& error);
+ void setSuccess(const APIUserAccount& user);
+
+ void cleanup();
+};
+
+
+class NexusKeyValidator
+{
+public:
+ enum Behaviour
+ {
+ OneShot = 0,
+ Retry
+ };
+
+ using FinishedCallback = void (
+ ValidationAttempt::Result, const QString&,
+ std::optional<APIUserAccount>);
+
+ std::function<FinishedCallback> finished;
+ std::function<void (const ValidationAttempt&)> attemptFinished;
+
+ NexusKeyValidator(NXMAccessManager& am);
+ ~NexusKeyValidator();
+
+ void start(const QString& key, Behaviour b);
+ void cancel();
+
+ bool isActive() const;
+ const ValidationAttempt* lastAttempt() const;
+ const ValidationAttempt* currentAttempt() const;
+
+private:
+ NXMAccessManager& m_manager;
+ QString m_key;
+ std::vector<std::unique_ptr<ValidationAttempt>> m_attempts;
+
+ void createAttempts(const std::vector<std::chrono::seconds>& timeouts);
+
+ bool nextTry();
+ void onAttemptSuccess(const ValidationAttempt& a, const APIUserAccount& u);
+ void onAttemptFailure(const ValidationAttempt& a);
+
+ void setFinished(
+ ValidationAttempt::Result r, const QString& message,
+ std::optional<APIUserAccount> user);
+};
+
+
+class ValidationProgressDialog : public QDialog
+{
+ Q_OBJECT;
+
+public:
+ ValidationProgressDialog(NexusKeyValidator& v);
+
+ void setParentWidget(QWidget* w);
+
+ void start();
+ void stop();
+
+protected:
+ void showEvent(QShowEvent* e) override;
+ void closeEvent(QCloseEvent* e) override;
+
+private:
+ std::unique_ptr<Ui::ValidationProgressDialog> ui;
+ NexusKeyValidator& m_validator;
+ QTimer* m_updateTimer;
+ bool m_first;
+
+ void onHide();
+ void onCancel();
+ void onTimer();
+ void updateProgress();
+};
+
/**
* @brief access manager extended to handle nxm links
@@ -36,10 +209,9 @@ class NXMAccessManager : public QNetworkAccessManager
{
Q_OBJECT
public:
+ NXMAccessManager(QObject *parent, const QString &moVersion);
- explicit NXMAccessManager(QObject *parent, const QString &moVersion);
-
- ~NXMAccessManager();
+ void setTopLevelWidget(QWidget* w);
bool validated() const;
@@ -53,12 +225,10 @@ public:
void clearCookies();
QString userAgent(const QString &subModule = QString()) const;
+ const QString& MOVersion() const;
- QString apiKey() const;
void clearApiKey();
- void startValidationCheck();
-
void refuseValidation();
signals:
@@ -76,17 +246,9 @@ signals:
* @param necessary true if a login was necessary and succeeded, false if the user is still logged in
**/
void validateSuccessful(bool necessary);
-
void validateFailed(const QString &message);
-
void credentialsReceived(const APIUserAccount& user);
-private slots:
-
- void validateFinished();
- void validateError(QNetworkReply::NetworkError errorCode);
- void validateTimeout();
-
protected:
virtual QNetworkReply *createRequest(
@@ -94,22 +256,29 @@ protected:
QIODevice *device);
private:
- QTimer m_ValidateTimeout;
- QNetworkReply *m_ValidateReply;
- QProgressDialog *m_ProgressDialog { nullptr };
+ enum States
+ {
+ NotChecked,
+ Valid,
+ Invalid
+ };
+ QWidget* m_TopLevel;
+ mutable std::unique_ptr<ValidationProgressDialog> m_ProgressDialog;
QString m_MOVersion;
+ NexusKeyValidator m_validator;
+ States m_validationState;
+
+ void startValidationCheck(const QString& key);
+
+ void onValidatorFinished(
+ ValidationAttempt::Result r, const QString& message,
+ std::optional<APIUserAccount>);
- QString m_ApiKey;
+ void onValidatorAttemptFinished(const ValidationAttempt& a);
- enum {
- VALIDATE_NOT_CHECKED,
- VALIDATE_CHECKING,
- VALIDATE_NOT_VALID,
- VALIDATE_ATTEMPT_FAILED,
- VALIDATE_REFUSED,
- VALIDATE_VALID
- } m_ValidateState = VALIDATE_NOT_CHECKED;
+ void startProgress();
+ void stopProgress();
};
#endif // NXMACCESSMANAGER_H
diff --git a/src/organizer_en.ts b/src/organizer_en.ts
index 1aa831a0..d30057aa 100644
--- a/src/organizer_en.ts
+++ b/src/organizer_en.ts
@@ -15,117 +15,122 @@
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="aboutdialog.ui" line="156"/>
- <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Source code can be found at &lt;a href=&quot;https://github.com/ModOrganizer2/modorganizer&quot;&gt;&lt;span style=&quot; text-decoration: underline; color:#007af4;&quot;&gt;GitHub&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <location filename="aboutdialog.ui" line="110"/>
+ <source>usvfs:</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="aboutdialog.ui" line="164"/>
+ <location filename="aboutdialog.ui" line="163"/>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Source code can be found at &lt;a href=&quot;https://github.com/ModOrganizer2/modorganizer&quot;&gt;GitHub&lt;/a&gt;.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="aboutdialog.ui" line="171"/>
<source>Used Software</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="aboutdialog.ui" line="177"/>
+ <location filename="aboutdialog.ui" line="184"/>
<source>Thanks</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="aboutdialog.ui" line="183"/>
+ <location filename="aboutdialog.ui" line="190"/>
<source>Lead Developers &amp;&amp; Maintainers</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="aboutdialog.ui" line="193"/>
+ <location filename="aboutdialog.ui" line="200"/>
<source>LePresidente (Project Lead)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="aboutdialog.ui" line="219"/>
+ <location filename="aboutdialog.ui" line="231"/>
<source>MO2 Developers &amp;&amp; Contributors</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="aboutdialog.ui" line="260"/>
+ <location filename="aboutdialog.ui" line="267"/>
<source>Translators</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="aboutdialog.ui" line="285"/>
+ <location filename="aboutdialog.ui" line="292"/>
<source>Cyb3r (Dutch)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="aboutdialog.ui" line="295"/>
+ <location filename="aboutdialog.ui" line="302"/>
<source>fruttyx (French)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="aboutdialog.ui" line="310"/>
+ <location filename="aboutdialog.ui" line="317"/>
<source>Yoplala (French)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="aboutdialog.ui" line="315"/>
+ <location filename="aboutdialog.ui" line="322"/>
<source>Faron (German)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="aboutdialog.ui" line="325"/>
+ <location filename="aboutdialog.ui" line="337"/>
<source>yohru (Japanese)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="aboutdialog.ui" line="330"/>
+ <location filename="aboutdialog.ui" line="342"/>
<source>Mordan (Greek)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="aboutdialog.ui" line="343"/>
+ <location filename="aboutdialog.ui" line="355"/>
<source>Yoosk (Polish)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="aboutdialog.ui" line="348"/>
+ <location filename="aboutdialog.ui" line="360"/>
<source>Brgodfx (Portuguese)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="aboutdialog.ui" line="353"/>
+ <location filename="aboutdialog.ui" line="365"/>
<source>zDas (Portuguese)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="aboutdialog.ui" line="373"/>
+ <location filename="aboutdialog.ui" line="385"/>
<source>Jax (Swedish)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="aboutdialog.ui" line="378"/>
+ <location filename="aboutdialog.ui" line="390"/>
<source>Nubbie (Swedish)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="aboutdialog.ui" line="383"/>
+ <location filename="aboutdialog.ui" line="395"/>
<source>...and all other contributors!</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="aboutdialog.ui" line="397"/>
+ <location filename="aboutdialog.ui" line="409"/>
<source>Other Supporters &amp;&amp; Contributors</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="aboutdialog.ui" line="407"/>
+ <location filename="aboutdialog.ui" line="419"/>
<source>Tannin (Original Creator)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="aboutdialog.ui" line="524"/>
+ <location filename="aboutdialog.ui" line="541"/>
<source>Close</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="aboutdialog.cpp" line="114"/>
+ <location filename="aboutdialog.cpp" line="117"/>
<source>No license</source>
<translation type="unfinished"></translation>
</message>
@@ -173,17 +178,17 @@ p, li { white-space: pre-wrap; }
<context>
<name>AdvancedConflictListModel</name>
<message>
- <location filename="modinfodialogconflicts.cpp" line="334"/>
+ <location filename="modinfodialogconflicts.cpp" line="337"/>
<source>Overwrites</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialogconflicts.cpp" line="335"/>
+ <location filename="modinfodialogconflicts.cpp" line="338"/>
<source>File</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialogconflicts.cpp" line="336"/>
+ <location filename="modinfodialogconflicts.cpp" line="339"/>
<source>Overwritten By</source>
<translation type="unfinished"></translation>
</message>
@@ -201,12 +206,12 @@ p, li { white-space: pre-wrap; }
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="browserdialog.cpp" line="94"/>
+ <location filename="browserdialog.cpp" line="95"/>
<source>new</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="browserdialog.cpp" line="207"/>
+ <location filename="browserdialog.cpp" line="208"/>
<source>failed to start download</source>
<translation type="unfinished"></translation>
</message>
@@ -276,12 +281,12 @@ p, li { white-space: pre-wrap; }
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="categoriesdialog.cpp" line="239"/>
+ <location filename="categoriesdialog.cpp" line="246"/>
<source>Add</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="categoriesdialog.cpp" line="240"/>
+ <location filename="categoriesdialog.cpp" line="247"/>
<source>Remove</source>
<translation type="unfinished"></translation>
</message>
@@ -289,33 +294,48 @@ p, li { white-space: pre-wrap; }
<context>
<name>ConflictsTab</name>
<message>
- <location filename="modinfodialogconflicts.cpp" line="695"/>
- <source>&amp;Hide</source>
+ <location filename="modinfodialogconflicts.cpp" line="774"/>
+ <source>&amp;Execute</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialogconflicts.cpp" line="700"/>
- <source>&amp;Unhide</source>
+ <location filename="modinfodialogconflicts.cpp" line="775"/>
+ <source>Execute with &amp;VFS</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialogconflicts.cpp" line="703"/>
- <source>&amp;Open/Execute</source>
+ <location filename="modinfodialogconflicts.cpp" line="777"/>
+ <source>&amp;Open</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialogconflicts.cpp" line="706"/>
+ <location filename="modinfodialogconflicts.cpp" line="778"/>
+ <source>Open with &amp;VFS</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="modinfodialogconflicts.cpp" line="781"/>
<source>&amp;Preview</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialogconflicts.cpp" line="709"/>
+ <location filename="modinfodialogconflicts.cpp" line="784"/>
+ <source>&amp;Go to...</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="modinfodialogconflicts.cpp" line="787"/>
<source>Open in &amp;Explorer</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialogconflicts.cpp" line="712"/>
- <source>&amp;Go to...</source>
+ <location filename="modinfodialogconflicts.cpp" line="790"/>
+ <source>&amp;Hide</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="modinfodialogconflicts.cpp" line="795"/>
+ <source>&amp;Unhide</source>
<translation type="unfinished"></translation>
</message>
</context>
@@ -368,114 +388,119 @@ p, li { white-space: pre-wrap; }
<context>
<name>DownloadList</name>
<message>
- <location filename="downloadlist.cpp" line="71"/>
+ <location filename="downloadlist.cpp" line="73"/>
<source>Name</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadlist.cpp" line="72"/>
+ <location filename="downloadlist.cpp" line="74"/>
<source>Mod name</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadlist.cpp" line="73"/>
+ <location filename="downloadlist.cpp" line="75"/>
<source>Version</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadlist.cpp" line="74"/>
+ <location filename="downloadlist.cpp" line="76"/>
<source>Nexus ID</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadlist.cpp" line="75"/>
+ <location filename="downloadlist.cpp" line="77"/>
<source>Size</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadlist.cpp" line="76"/>
+ <location filename="downloadlist.cpp" line="78"/>
<source>Status</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadlist.cpp" line="77"/>
+ <location filename="downloadlist.cpp" line="79"/>
<source>Filetime</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadlist.cpp" line="92"/>
+ <location filename="downloadlist.cpp" line="80"/>
+ <source>Source Game</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="downloadlist.cpp" line="95"/>
<source>&lt; game %1 mod %2 file %3 &gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadlist.cpp" line="93"/>
+ <location filename="downloadlist.cpp" line="96"/>
<source>Unknown</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadlist.cpp" line="94"/>
+ <location filename="downloadlist.cpp" line="97"/>
<source>Pending</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadlist.cpp" line="128"/>
+ <location filename="downloadlist.cpp" line="137"/>
<source>Started</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadlist.cpp" line="129"/>
+ <location filename="downloadlist.cpp" line="138"/>
<source>Canceling</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadlist.cpp" line="130"/>
+ <location filename="downloadlist.cpp" line="139"/>
<source>Pausing</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadlist.cpp" line="131"/>
+ <location filename="downloadlist.cpp" line="140"/>
<source>Canceled</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadlist.cpp" line="132"/>
+ <location filename="downloadlist.cpp" line="141"/>
<source>Paused</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadlist.cpp" line="133"/>
+ <location filename="downloadlist.cpp" line="142"/>
<source>Error</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadlist.cpp" line="134"/>
- <location filename="downloadlist.cpp" line="135"/>
- <location filename="downloadlist.cpp" line="136"/>
+ <location filename="downloadlist.cpp" line="143"/>
+ <location filename="downloadlist.cpp" line="144"/>
+ <location filename="downloadlist.cpp" line="145"/>
<source>Fetching Info</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadlist.cpp" line="137"/>
+ <location filename="downloadlist.cpp" line="146"/>
<source>Downloaded</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadlist.cpp" line="138"/>
+ <location filename="downloadlist.cpp" line="147"/>
<source>Installed</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadlist.cpp" line="139"/>
+ <location filename="downloadlist.cpp" line="148"/>
<source>Uninstalled</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadlist.cpp" line="157"/>
+ <location filename="downloadlist.cpp" line="166"/>
<source>Pending download</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadlist.cpp" line="161"/>
+ <location filename="downloadlist.cpp" line="170"/>
<source>Information missing, please select &quot;Query Info&quot; from the context menu to re-retrieve.</source>
<translation type="unfinished"></translation>
</message>
@@ -483,156 +508,153 @@ p, li { white-space: pre-wrap; }
<context>
<name>DownloadListWidget</name>
<message>
- <location filename="downloadlistwidget.cpp" line="210"/>
+ <location filename="downloadlistwidget.cpp" line="217"/>
<source>Install</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadlistwidget.cpp" line="212"/>
+ <location filename="downloadlistwidget.cpp" line="219"/>
<source>Query Info</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadlistwidget.cpp" line="214"/>
+ <location filename="downloadlistwidget.cpp" line="221"/>
<source>Visit on Nexus</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadlistwidget.cpp" line="215"/>
+ <location filename="downloadlistwidget.cpp" line="222"/>
<source>Open File</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadlistwidget.cpp" line="216"/>
- <location filename="downloadlistwidget.cpp" line="228"/>
- <location filename="downloadlistwidget.cpp" line="233"/>
+ <location filename="downloadlistwidget.cpp" line="223"/>
+ <location filename="downloadlistwidget.cpp" line="235"/>
+ <location filename="downloadlistwidget.cpp" line="240"/>
<source>Show in Folder</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadlistwidget.cpp" line="220"/>
- <location filename="downloadlistwidget.cpp" line="231"/>
+ <location filename="downloadlistwidget.cpp" line="227"/>
+ <location filename="downloadlistwidget.cpp" line="238"/>
<source>Delete</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadlistwidget.cpp" line="222"/>
+ <location filename="downloadlistwidget.cpp" line="229"/>
<source>Un-Hide</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadlistwidget.cpp" line="224"/>
+ <location filename="downloadlistwidget.cpp" line="231"/>
<source>Hide</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadlistwidget.cpp" line="226"/>
+ <location filename="downloadlistwidget.cpp" line="233"/>
<source>Cancel</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadlistwidget.cpp" line="227"/>
+ <location filename="downloadlistwidget.cpp" line="234"/>
<source>Pause</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadlistwidget.cpp" line="232"/>
+ <location filename="downloadlistwidget.cpp" line="239"/>
<source>Resume</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadlistwidget.cpp" line="238"/>
+ <location filename="downloadlistwidget.cpp" line="251"/>
<source>Delete Installed Downloads...</source>
- <oldsource>Delete Installed...</oldsource>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadlistwidget.cpp" line="239"/>
+ <location filename="downloadlistwidget.cpp" line="252"/>
<source>Delete Uninstalled Downloads...</source>
- <oldsource>Delete Uninstalled...</oldsource>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadlistwidget.cpp" line="240"/>
+ <location filename="downloadlistwidget.cpp" line="253"/>
<source>Delete All Downloads...</source>
- <oldsource>Delete All...</oldsource>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadlistwidget.cpp" line="244"/>
+ <location filename="downloadlistwidget.cpp" line="257"/>
<source>Hide Installed...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadlistwidget.cpp" line="245"/>
+ <location filename="downloadlistwidget.cpp" line="258"/>
<source>Hide Uninstalled...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadlistwidget.cpp" line="246"/>
+ <location filename="downloadlistwidget.cpp" line="259"/>
<source>Hide All...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadlistwidget.cpp" line="248"/>
+ <location filename="downloadlistwidget.cpp" line="261"/>
<source>Un-Hide All...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadlistwidget.cpp" line="271"/>
- <location filename="downloadlistwidget.cpp" line="326"/>
- <location filename="downloadlistwidget.cpp" line="335"/>
- <location filename="downloadlistwidget.cpp" line="344"/>
+ <location filename="downloadlistwidget.cpp" line="284"/>
+ <location filename="downloadlistwidget.cpp" line="339"/>
+ <location filename="downloadlistwidget.cpp" line="348"/>
+ <location filename="downloadlistwidget.cpp" line="357"/>
<source>Delete Files?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadlistwidget.cpp" line="272"/>
+ <location filename="downloadlistwidget.cpp" line="285"/>
<source>This will permanently delete the selected download.
Are you absolutely sure you want to proceed?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadlistwidget.cpp" line="327"/>
+ <location filename="downloadlistwidget.cpp" line="340"/>
<source>This will remove all finished downloads from this list and from disk.
Are you absolutely sure you want to proceed?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadlistwidget.cpp" line="336"/>
+ <location filename="downloadlistwidget.cpp" line="349"/>
<source>This will remove all installed downloads from this list and from disk.
Are you absolutely sure you want to proceed?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadlistwidget.cpp" line="345"/>
+ <location filename="downloadlistwidget.cpp" line="358"/>
<source>This will remove all uninstalled downloads from this list and from disk.
Are you absolutely sure you want to proceed?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadlistwidget.cpp" line="353"/>
- <location filename="downloadlistwidget.cpp" line="362"/>
- <location filename="downloadlistwidget.cpp" line="371"/>
+ <location filename="downloadlistwidget.cpp" line="366"/>
+ <location filename="downloadlistwidget.cpp" line="375"/>
+ <location filename="downloadlistwidget.cpp" line="384"/>
<source>Hide Files?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadlistwidget.cpp" line="354"/>
+ <location filename="downloadlistwidget.cpp" line="367"/>
<source>This will remove all finished downloads from this list (but NOT from disk).</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadlistwidget.cpp" line="363"/>
+ <location filename="downloadlistwidget.cpp" line="376"/>
<source>This will remove all installed downloads from this list (but NOT from disk).</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadlistwidget.cpp" line="372"/>
+ <location filename="downloadlistwidget.cpp" line="385"/>
<source>This will remove all uninstalled downloads from this list (but NOT from disk).</source>
<translation type="unfinished"></translation>
</message>
@@ -645,37 +667,37 @@ Are you absolutely sure you want to proceed?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="390"/>
+ <location filename="downloadmanager.cpp" line="386"/>
<source>Memory allocation error (in refreshing directory).</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="490"/>
+ <location filename="downloadmanager.cpp" line="486"/>
<source>failed to download %1: could not open output file: %2</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="513"/>
+ <location filename="downloadmanager.cpp" line="509"/>
<source>Download again?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="513"/>
+ <location filename="downloadmanager.cpp" line="509"/>
<source>A file with the same name &quot;%1&quot; has already been downloaded. Do you want to download it again? The new file will receive a different name.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="568"/>
+ <location filename="downloadmanager.cpp" line="564"/>
<source>Wrong Game</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="568"/>
+ <location filename="downloadmanager.cpp" line="564"/>
<source>The download link is for a mod for &quot;%1&quot; but this instance of MO has been set up for &quot;%2&quot;.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="576"/>
+ <location filename="downloadmanager.cpp" line="572"/>
<source>There is already a download queued for this file.
Mod %1
@@ -683,12 +705,12 @@ File %2</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="582"/>
+ <location filename="downloadmanager.cpp" line="579"/>
<source>Already Queued</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="591"/>
+ <location filename="downloadmanager.cpp" line="588"/>
<source>There is already a download started for this file.
Mod %1: %2
@@ -696,266 +718,277 @@ File %3: %4</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="626"/>
+ <location filename="downloadmanager.cpp" line="623"/>
<source>Already Started</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="655"/>
- <location filename="downloadmanager.cpp" line="788"/>
+ <location filename="downloadmanager.cpp" line="652"/>
+ <location filename="downloadmanager.cpp" line="785"/>
<source>remove: invalid download index %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="674"/>
+ <location filename="downloadmanager.cpp" line="671"/>
<source>failed to delete %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="681"/>
+ <location filename="downloadmanager.cpp" line="678"/>
<source>failed to delete meta file for %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="741"/>
+ <location filename="downloadmanager.cpp" line="738"/>
<source>restore: invalid download index: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="810"/>
+ <location filename="downloadmanager.cpp" line="807"/>
<source>cancel: invalid download index %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="823"/>
+ <location filename="downloadmanager.cpp" line="820"/>
<source>pause: invalid download index %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="845"/>
+ <location filename="downloadmanager.cpp" line="842"/>
<source>resume: invalid download index %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="856"/>
+ <location filename="downloadmanager.cpp" line="853"/>
<source>resume (int): invalid download index %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="880"/>
+ <location filename="downloadmanager.cpp" line="877"/>
<source>No known download urls. Sorry, this download can&apos;t be resumed.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="921"/>
- <location filename="downloadmanager.cpp" line="973"/>
+ <location filename="downloadmanager.cpp" line="918"/>
+ <location filename="downloadmanager.cpp" line="970"/>
<source>query: invalid download index %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="943"/>
+ <location filename="downloadmanager.cpp" line="940"/>
<source>Please enter the nexus mod id</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="943"/>
+ <location filename="downloadmanager.cpp" line="940"/>
<source>Mod ID:</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="954"/>
+ <location filename="downloadmanager.cpp" line="951"/>
<source>Please select the source game code for %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="1013"/>
+ <location filename="downloadmanager.cpp" line="1003"/>
+ <source>Hashing download file &apos;%1&apos;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="downloadmanager.cpp" line="1004"/>
+ <source>Cancel</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="downloadmanager.cpp" line="1033"/>
<source>VisitNexus: invalid download index %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="1034"/>
+ <location filename="downloadmanager.cpp" line="1054"/>
<source>Nexus ID for this Mod is unknown</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="1041"/>
+ <location filename="downloadmanager.cpp" line="1061"/>
<source>OpenFile: invalid download index %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="1058"/>
+ <location filename="downloadmanager.cpp" line="1078"/>
<source>OpenFileInDownloadsFolder: invalid download index %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="1093"/>
+ <location filename="downloadmanager.cpp" line="1113"/>
<source>get pending: invalid download index %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="1102"/>
+ <location filename="downloadmanager.cpp" line="1122"/>
<source>get path: invalid download index %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="1111"/>
+ <location filename="downloadmanager.cpp" line="1131"/>
<source>Main</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="1112"/>
+ <location filename="downloadmanager.cpp" line="1132"/>
<source>Update</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="1113"/>
+ <location filename="downloadmanager.cpp" line="1133"/>
<source>Optional</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="1114"/>
+ <location filename="downloadmanager.cpp" line="1134"/>
<source>Old</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="1115"/>
+ <location filename="downloadmanager.cpp" line="1135"/>
<source>Miscellaneous</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="1116"/>
+ <location filename="downloadmanager.cpp" line="1136"/>
<source>Deleted</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="1117"/>
+ <location filename="downloadmanager.cpp" line="1137"/>
<source>Unknown</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="1124"/>
+ <location filename="downloadmanager.cpp" line="1144"/>
<source>display name: invalid download index %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="1144"/>
+ <location filename="downloadmanager.cpp" line="1164"/>
<source>file name: invalid download index %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="1153"/>
+ <location filename="downloadmanager.cpp" line="1173"/>
<source>file time: invalid download index %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="1167"/>
+ <location filename="downloadmanager.cpp" line="1192"/>
<source>file size: invalid download index %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="1177"/>
+ <location filename="downloadmanager.cpp" line="1202"/>
<source>progress: invalid download index %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="1187"/>
+ <location filename="downloadmanager.cpp" line="1212"/>
<source>state: invalid download index %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="1197"/>
+ <location filename="downloadmanager.cpp" line="1222"/>
<source>infocomplete: invalid download index %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="1212"/>
- <location filename="downloadmanager.cpp" line="1220"/>
+ <location filename="downloadmanager.cpp" line="1237"/>
+ <location filename="downloadmanager.cpp" line="1245"/>
+ <location filename="downloadmanager.cpp" line="1258"/>
<source>mod id: invalid download index %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="1228"/>
+ <location filename="downloadmanager.cpp" line="1266"/>
<source>ishidden: invalid download index %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="1237"/>
+ <location filename="downloadmanager.cpp" line="1275"/>
<source>file info: invalid download index %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="1247"/>
+ <location filename="downloadmanager.cpp" line="1285"/>
<source>mark installed: invalid download index %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="1292"/>
+ <location filename="downloadmanager.cpp" line="1330"/>
<source>mark uninstalled: invalid download index %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="1469"/>
+ <location filename="downloadmanager.cpp" line="1496"/>
<source>Memory allocation error (in processing progress event).</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="1479"/>
+ <location filename="downloadmanager.cpp" line="1506"/>
<source>Memory allocation error (in processing downloaded data).</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="1595"/>
+ <location filename="downloadmanager.cpp" line="1622"/>
<source>Information updated</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="1597"/>
- <location filename="downloadmanager.cpp" line="1612"/>
+ <location filename="downloadmanager.cpp" line="1624"/>
+ <location filename="downloadmanager.cpp" line="1641"/>
<source>No matching file found on Nexus! Maybe this file is no longer available or it was renamed?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="1599"/>
+ <location filename="downloadmanager.cpp" line="1626"/>
<source>No file on Nexus matches the selected file by name. Please manually choose the correct one.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="1727"/>
+ <location filename="downloadmanager.cpp" line="1782"/>
<source>No download server available. Please try again later.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="1892"/>
+ <location filename="downloadmanager.cpp" line="1952"/>
<source>Failed to request file info from nexus: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="1919"/>
+ <location filename="downloadmanager.cpp" line="1979"/>
<source>Warning: Content type is: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="1924"/>
+ <location filename="downloadmanager.cpp" line="1984"/>
<source>Download header content length: %1 downloaded file size: %2</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="1926"/>
+ <location filename="downloadmanager.cpp" line="1986"/>
<source>Download failed: %1 (%2)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="1948"/>
+ <location filename="downloadmanager.cpp" line="2008"/>
<source>We were unable to download the file due to errors after four retries. There may be an issue with the Nexus servers.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="2031"/>
+ <location filename="downloadmanager.cpp" line="2091"/>
<source>failed to re-open %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="downloadmanager.cpp" line="2072"/>
+ <location filename="downloadmanager.cpp" line="2136"/>
<source>Unable to write download to drive (return %1).
Check the drive&apos;s available storage.
@@ -988,19 +1021,18 @@ Canceling download &quot;%2&quot;...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="editexecutablesdialog.ui" line="114"/>
<location filename="editexecutablesdialog.ui" line="117"/>
<location filename="editexecutablesdialog.ui" line="120"/>
+ <location filename="editexecutablesdialog.ui" line="123"/>
<source>Remove the selected executable</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="editexecutablesdialog.ui" line="123"/>
+ <location filename="editexecutablesdialog.ui" line="126"/>
<source>Remove</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="editexecutablesdialog.ui" line="134"/>
<location filename="editexecutablesdialog.ui" line="137"/>
<location filename="editexecutablesdialog.ui" line="140"/>
<location filename="editexecutablesdialog.ui" line="143"/>
@@ -1008,7 +1040,11 @@ Canceling download &quot;%2&quot;...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="editexecutablesdialog.ui" line="154"/>
+ <location filename="editexecutablesdialog.ui" line="146"/>
+ <source>Up</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
<location filename="editexecutablesdialog.ui" line="157"/>
<location filename="editexecutablesdialog.ui" line="160"/>
<location filename="editexecutablesdialog.ui" line="163"/>
@@ -1016,259 +1052,345 @@ Canceling download &quot;%2&quot;...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="editexecutablesdialog.ui" line="174"/>
+ <location filename="editexecutablesdialog.ui" line="166"/>
+ <source>Down</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
<location filename="editexecutablesdialog.ui" line="177"/>
<location filename="editexecutablesdialog.ui" line="180"/>
+ <location filename="editexecutablesdialog.ui" line="183"/>
<source>Adds the executables provided by the game plugin and moves any existing executables out of the way</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="editexecutablesdialog.ui" line="183"/>
+ <location filename="editexecutablesdialog.ui" line="186"/>
<source>Reset</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="editexecutablesdialog.ui" line="196"/>
+ <location filename="editexecutablesdialog.ui" line="199"/>
<source>List of configured executables</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="editexecutablesdialog.ui" line="199"/>
+ <location filename="editexecutablesdialog.ui" line="202"/>
<source>This is a list of your configured executables. Executables in grey are automatically recognised and can not be modified.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="editexecutablesdialog.ui" line="248"/>
+ <location filename="editexecutablesdialog.ui" line="251"/>
<source>Title</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="editexecutablesdialog.ui" line="255"/>
<location filename="editexecutablesdialog.ui" line="258"/>
+ <location filename="editexecutablesdialog.ui" line="261"/>
<source>Name of the executable. This is only for display purposes.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="editexecutablesdialog.ui" line="265"/>
+ <location filename="editexecutablesdialog.ui" line="268"/>
<source>Binary</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="editexecutablesdialog.ui" line="274"/>
<location filename="editexecutablesdialog.ui" line="277"/>
+ <location filename="editexecutablesdialog.ui" line="280"/>
<source>Binary to run</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="editexecutablesdialog.ui" line="284"/>
+ <location filename="editexecutablesdialog.ui" line="287"/>
<source>Browse filesystem</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="editexecutablesdialog.ui" line="287"/>
+ <location filename="editexecutablesdialog.ui" line="290"/>
<source>Browse filesystem for the executable to run.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="editexecutablesdialog.ui" line="290"/>
- <location filename="editexecutablesdialog.ui" line="311"/>
+ <location filename="editexecutablesdialog.ui" line="293"/>
+ <location filename="editexecutablesdialog.ui" line="314"/>
<source>...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="editexecutablesdialog.ui" line="299"/>
+ <location filename="editexecutablesdialog.ui" line="302"/>
<source>Start in</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="editexecutablesdialog.ui" line="320"/>
+ <location filename="editexecutablesdialog.ui" line="323"/>
<source>Arguments</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="editexecutablesdialog.ui" line="327"/>
<location filename="editexecutablesdialog.ui" line="330"/>
+ <location filename="editexecutablesdialog.ui" line="333"/>
<source>Arguments to pass to the application</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="editexecutablesdialog.ui" line="341"/>
+ <location filename="editexecutablesdialog.ui" line="344"/>
<source>Allow the Steam AppID to be used for this executable to be changed.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="editexecutablesdialog.ui" line="344"/>
+ <location filename="editexecutablesdialog.ui" line="347"/>
<source>Allow the Steam AppID to be used for this executable to be changed.
Every game/tool distributed through Steam has a unique ID. MO needs to know this ID to start those programs directly, otherwise the program is started by steam and then MO will not work. By default, MO will use the AppID for the game.
Right now the only case I know of where this needs to be overwritten is for the Skyrim Creation Kit which has its own AppID. This overwrite is already preconfigured.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="editexecutablesdialog.ui" line="349"/>
+ <location filename="editexecutablesdialog.ui" line="352"/>
<source>Overwrite Steam AppID</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="editexecutablesdialog.ui" line="359"/>
+ <location filename="editexecutablesdialog.ui" line="362"/>
<source>Steam AppID to use for this executable that differs from the games AppID.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="editexecutablesdialog.ui" line="362"/>
+ <location filename="editexecutablesdialog.ui" line="365"/>
<source>Steam AppID to use for this executable that differs from the games AppID.
Every game/tool distributed through Steam has a unique ID. MO needs to know this ID to start those programs directly, otherwise the program is started by steam and then MO will not work. By default, MO will use the AppID for the game (usually 72850).
Right now the only case I know of where this needs to be overwritten is for the Skyrim Creation Kit which has its own AppID (usually 202480). This overwrite is already preconfigured.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="editexecutablesdialog.ui" line="375"/>
+ <location filename="editexecutablesdialog.ui" line="378"/>
<source>If this is enabled, new files are created in the specified mod instead of the &quot;Overwrite&quot; mod.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="editexecutablesdialog.ui" line="378"/>
- <source>Create Files in Mod instead of Overwrite (*)</source>
+ <location filename="editexecutablesdialog.ui" line="381"/>
+ <source>Create files in mod instead of overwrite (*)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="editexecutablesdialog.ui" line="396"/>
+ <location filename="editexecutablesdialog.ui" line="399"/>
<source>If this is enabled, the configured libraries will be automatically loaded when this executable is launched.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="editexecutablesdialog.ui" line="399"/>
- <source>Force Load Libraries (*)</source>
+ <location filename="editexecutablesdialog.ui" line="402"/>
+ <source>Force load libraries (*)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="editexecutablesdialog.ui" line="422"/>
+ <location filename="editexecutablesdialog.ui" line="425"/>
<source>Configure Libraries</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="editexecutablesdialog.ui" line="431"/>
- <source>Use Application&apos;s Icon for shortcuts</source>
+ <location filename="editexecutablesdialog.ui" line="434"/>
+ <source>Use application&apos;s icon for desktop shortcuts</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="editexecutablesdialog.ui" line="441"/>
+ <location filename="editexecutablesdialog.ui" line="444"/>
+ <source>This executable will not appear in the list, on the toolbar or in the menu. It will still be visible in this dialog.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="editexecutablesdialog.ui" line="438"/>
- <source>(*) Profile Specific</source>
+ <location filename="editexecutablesdialog.ui" line="447"/>
+ <source>Hide in user interface</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="editexecutablesdialog.cpp" line="437"/>
+ <location filename="editexecutablesdialog.ui" line="454"/>
+ <source>(*) Profile specific</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="editexecutablesdialog.cpp" line="89"/>
+ <source>Add from file...</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="editexecutablesdialog.cpp" line="90"/>
+ <source>Add empty</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="editexecutablesdialog.cpp" line="91"/>
+ <source>Clone selected</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="editexecutablesdialog.cpp" line="500"/>
<source>Reset plugin executables</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="editexecutablesdialog.cpp" line="439"/>
+ <location filename="editexecutablesdialog.cpp" line="502"/>
<source>This will restore all the executables provided by the game plugin. If there are existing executables with the same names, they will be automatically renamed and left unchanged.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="editexecutablesdialog.cpp" line="458"/>
+ <location filename="editexecutablesdialog.cpp" line="687"/>
+ <location filename="editexecutablesdialog.cpp" line="729"/>
<source>New Executable</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="editexecutablesdialog.cpp" line="602"/>
- <source>Select a binary</source>
+ <location filename="editexecutablesdialog.cpp" line="742"/>
+ <source>Select a directory</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="editexecutablesdialog.cpp" line="603"/>
- <source>Executable (%1)</source>
+ <location filename="editexecutablesdialog.cpp" line="788"/>
+ <source>Executables (*.exe *.bat *.jar)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="editexecutablesdialog.cpp" line="634"/>
- <source>Select a directory</source>
+ <location filename="editexecutablesdialog.cpp" line="789"/>
+ <source>All Files (*.*)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="editexecutablesdialog.cpp" line="792"/>
+ <source>Select an executable</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="editexecutablesdialog.cpp" line="683"/>
- <source>Java (32-bit) required</source>
+ <location filename="editexecutablesdialog.cpp" line="808"/>
+ <source>Java required</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="editexecutablesdialog.cpp" line="684"/>
- <source>MO requires 32-bit java to run this application. If you already have it installed, select javaw.exe from that installation as the binary.</source>
+ <location filename="editexecutablesdialog.cpp" line="809"/>
+ <source>MO requires Java to run this application. If you already have it installed, select javaw.exe from that installation as the binary.</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>FileTreeTab</name>
<message>
- <location filename="modinfodialogfiletree.cpp" line="24"/>
+ <location filename="modinfodialogfiletree.cpp" line="25"/>
<source>&amp;New Folder</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialogfiletree.cpp" line="25"/>
+ <location filename="modinfodialogfiletree.cpp" line="26"/>
<source>&amp;Open/Execute</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialogfiletree.cpp" line="26"/>
+ <location filename="modinfodialogfiletree.cpp" line="27"/>
+ <location filename="modinfodialogfiletree.cpp" line="478"/>
+ <source>Open with &amp;VFS</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="modinfodialogfiletree.cpp" line="28"/>
<source>&amp;Preview</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialogfiletree.cpp" line="27"/>
+ <location filename="modinfodialogfiletree.cpp" line="29"/>
<source>Open in &amp;Explorer</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialogfiletree.cpp" line="28"/>
+ <location filename="modinfodialogfiletree.cpp" line="30"/>
<source>&amp;Rename</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialogfiletree.cpp" line="29"/>
+ <location filename="modinfodialogfiletree.cpp" line="31"/>
<source>&amp;Delete</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialogfiletree.cpp" line="30"/>
+ <location filename="modinfodialogfiletree.cpp" line="32"/>
<source>&amp;Hide</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialogfiletree.cpp" line="31"/>
+ <location filename="modinfodialogfiletree.cpp" line="33"/>
<source>&amp;Unhide</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialogfiletree.cpp" line="103"/>
- <location filename="modinfodialogfiletree.cpp" line="109"/>
+ <location filename="modinfodialogfiletree.cpp" line="124"/>
+ <location filename="modinfodialogfiletree.cpp" line="130"/>
<source>New Folder</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialogfiletree.cpp" line="115"/>
+ <location filename="modinfodialogfiletree.cpp" line="136"/>
<source>Failed to create &quot;%1&quot;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialogfiletree.cpp" line="180"/>
+ <location filename="modinfodialogfiletree.cpp" line="243"/>
<source>Are you sure you want to delete &quot;%1&quot;?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialogfiletree.cpp" line="182"/>
+ <location filename="modinfodialogfiletree.cpp" line="245"/>
<source>Are you sure you want to delete the selected files?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialogfiletree.cpp" line="185"/>
+ <location filename="modinfodialogfiletree.cpp" line="248"/>
<source>Confirm</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialogfiletree.cpp" line="220"/>
+ <location filename="modinfodialogfiletree.cpp" line="283"/>
<source>Failed to delete %1</source>
<translation type="unfinished"></translation>
</message>
+ <message>
+ <location filename="modinfodialogfiletree.cpp" line="474"/>
+ <source>&amp;Execute</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="modinfodialogfiletree.cpp" line="475"/>
+ <source>Execute with &amp;VFS</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="modinfodialogfiletree.cpp" line="477"/>
+ <source>&amp;Open</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>FilterList</name>
+ <message>
+ <location filename="filterlist.cpp" line="104"/>
+ <source>Not</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="filterlist.cpp" line="207"/>
+ <source>Filter separators</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="filterlist.cpp" line="208"/>
+ <source>Show separators</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="filterlist.cpp" line="209"/>
+ <source>Hide separators</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="filterlist.cpp" line="242"/>
+ <source>Contains %1</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>FindDialog</name>
@@ -1405,83 +1527,101 @@ Right now the only case I know of where this needs to be overwritten is for the
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="installationmanager.cpp" line="489"/>
- <source>failed to create backup</source>
+ <location filename="installationmanager.cpp" line="312"/>
+ <location filename="installationmanager.cpp" line="318"/>
+ <location filename="installationmanager.cpp" line="623"/>
+ <location filename="installationmanager.cpp" line="628"/>
+ <source>Extraction failed: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="installationmanager.cpp" line="498"/>
+ <source>Failed to create backup</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="installationmanager.cpp" line="507"/>
<source>Mod Name</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="installationmanager.cpp" line="498"/>
+ <location filename="installationmanager.cpp" line="507"/>
<source>Name</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="installationmanager.cpp" line="551"/>
+ <location filename="installationmanager.cpp" line="560"/>
<source>Invalid name</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="installationmanager.cpp" line="552"/>
+ <location filename="installationmanager.cpp" line="561"/>
<source>The name you entered is invalid, please enter a different one.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="installationmanager.cpp" line="720"/>
+ <location filename="installationmanager.cpp" line="729"/>
<source>File format &quot;%1&quot; not supported</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="installationmanager.cpp" line="884"/>
+ <location filename="installationmanager.cpp" line="872"/>
+ <source>Installation failed</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="installationmanager.cpp" line="873"/>
+ <source>Something went wrong while installing this mod.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="installationmanager.cpp" line="895"/>
<source>None of the available installer plugins were able to handle that archive.
This is likely due to a corrupted or incompatible download or unrecognized archive format.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="installationmanager.cpp" line="896"/>
+ <location filename="installationmanager.cpp" line="908"/>
<source>no error</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="installationmanager.cpp" line="899"/>
+ <location filename="installationmanager.cpp" line="911"/>
<source>7z.dll not found</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="installationmanager.cpp" line="902"/>
+ <location filename="installationmanager.cpp" line="914"/>
<source>7z.dll isn&apos;t valid</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="installationmanager.cpp" line="905"/>
+ <location filename="installationmanager.cpp" line="917"/>
<source>archive not found</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="installationmanager.cpp" line="908"/>
+ <location filename="installationmanager.cpp" line="920"/>
<source>failed to open archive</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="installationmanager.cpp" line="911"/>
+ <location filename="installationmanager.cpp" line="923"/>
<source>unsupported archive type</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="installationmanager.cpp" line="914"/>
+ <location filename="installationmanager.cpp" line="926"/>
<source>internal library error</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="installationmanager.cpp" line="917"/>
+ <location filename="installationmanager.cpp" line="929"/>
<source>archive invalid</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="installationmanager.cpp" line="921"/>
+ <location filename="installationmanager.cpp" line="933"/>
<source>unknown archive error</source>
<translation type="unfinished"></translation>
</message>
@@ -1500,45 +1640,124 @@ This is likely due to a corrupted or incompatible download or unrecognized archi
</message>
</context>
<context>
- <name>LockedDialog</name>
+ <name>LogList</name>
<message>
- <location filename="lockeddialog.ui" line="14"/>
- <source>Running virtualized processes</source>
+ <location filename="loglist.cpp" line="219"/>
+ <source>&amp;Copy all</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="lockeddialog.ui" line="20"/>
- <source>This dialog should disappear automatically if the application/game is done. Click unlock if it didn&apos;t.</source>
+ <location filename="loglist.cpp" line="221"/>
+ <source>C&amp;lear all</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="lockeddialog.ui" line="23"/>
- <source>MO is locked while the executable is running.</source>
+ <location filename="loglist.cpp" line="223"/>
+ <source>&amp;Level</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="lockeddialog.ui" line="78"/>
- <source>Unlock</source>
+ <location filename="loglist.cpp" line="243"/>
+ <source>&amp;Debug</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="loglist.cpp" line="244"/>
+ <source>&amp;Info</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="loglist.cpp" line="245"/>
+ <source>&amp;Warnings</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="loglist.cpp" line="246"/>
+ <source>&amp;Errors</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
- <name>LogBuffer</name>
+ <name>Loot</name>
+ <message>
+ <location filename="loot.cpp" line="239"/>
+ <source>Loot failed to run</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="loot.cpp" line="242"/>
+ <source>No errors were reported. The log below might have more information.
+</source>
+ <translation type="unfinished"></translation>
+ </message>
<message>
- <location filename="logbuffer.cpp" line="86"/>
- <source>failed to write log to %1: %2</source>
+ <location filename="loot.cpp" line="296"/>
+ <source>Errors</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="loot.cpp" line="308"/>
+ <source>Warnings</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="loot.cpp" line="517"/>
+ <source>failed to start loot</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="loot.cpp" line="632"/>
+ <source>Loot failed. Exit code was: %1</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>LootDialog</name>
+ <message>
+ <location filename="lootdialog.ui" line="14"/>
+ <source>LOOT</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="lootdialog.ui" line="50"/>
+ <source>Progress</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="lootdialog.ui" line="89"/>
+ <source>about:blank</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="lootdialog.ui" line="118"/>
+ <source>Details</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="lootdialog.ui" line="155"/>
+ <source>Open JSON report</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="lootdialog.cpp" line="139"/>
+ <source>Stopping LOOT...</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="lootdialog.cpp" line="215"/>
+ <source>Running LOOT...</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>MOApplication</name>
<message>
- <location filename="moapplication.cpp" line="119"/>
+ <location filename="moapplication.cpp" line="121"/>
<source>an error occurred: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="moapplication.cpp" line="124"/>
+ <location filename="moapplication.cpp" line="127"/>
<source>an error occurred</source>
<translation type="unfinished"></translation>
</message>
@@ -1546,27 +1765,27 @@ This is likely due to a corrupted or incompatible download or unrecognized archi
<context>
<name>MOBase::TextViewer</name>
<message>
- <location filename="../../uibase/src/textviewer.cpp" line="58"/>
+ <location filename="../../uibase/src/textviewer.cpp" line="59"/>
<source>Save changes?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../../uibase/src/textviewer.cpp" line="59"/>
+ <location filename="../../uibase/src/textviewer.cpp" line="60"/>
<source>Do you want to save changes to %1?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../../uibase/src/textviewer.cpp" line="142"/>
+ <location filename="../../uibase/src/textviewer.cpp" line="159"/>
<source>failed to write to %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../../uibase/src/textviewer.cpp" line="178"/>
+ <location filename="../../uibase/src/textviewer.cpp" line="199"/>
<source>file not found: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../../uibase/src/textviewer.cpp" line="203"/>
+ <location filename="../../uibase/src/textviewer.cpp" line="230"/>
<source>Save</source>
<translation type="unfinished"></translation>
</message>
@@ -1574,7 +1793,7 @@ This is likely due to a corrupted or incompatible download or unrecognized archi
<context>
<name>MOBase::TutorialControl</name>
<message>
- <location filename="../../uibase/src/tutorialcontrol.cpp" line="146"/>
+ <location filename="../../uibase/src/tutorialcontrol.cpp" line="148"/>
<source>Tutorial failed to start, please check &quot;mo_interface.log&quot; for details.</source>
<translation type="unfinished"></translation>
</message>
@@ -1590,48 +1809,59 @@ This is likely due to a corrupted or incompatible download or unrecognized archi
<context>
<name>MainWindow</name>
<message>
- <location filename="mainwindow.ui" line="61"/>
- <location filename="mainwindow.ui" line="536"/>
- <source>Categories</source>
+ <location filename="mainwindow.ui" line="53"/>
+ <source>Filters</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="133"/>
+ <location filename="mainwindow.ui" line="139"/>
<source>Clear</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="152"/>
- <source>If checked, only mods that match all selected categories are displayed.</source>
+ <location filename="mainwindow.ui" line="146"/>
+ <source>Edit...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="155"/>
+ <location filename="mainwindow.ui" line="177"/>
+ <source>Display mods that match all selected categories.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="mainwindow.ui" line="180"/>
<source>And</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="165"/>
- <source>If checked, all mods that match at least one of the selected categories are displayed.</source>
+ <location filename="mainwindow.ui" line="190"/>
+ <source>Display mods that match at least one of the selected categories</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="168"/>
+ <location filename="mainwindow.ui" line="193"/>
<source>Or</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="207"/>
+ <location filename="mainwindow.ui" line="200"/>
+ <source>Filter: only show the separators that match the current filters
+Show: always show separators
+Hide: never show separators</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="mainwindow.ui" line="237"/>
<source>Profile</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="217"/>
+ <location filename="mainwindow.ui" line="247"/>
<source>Pick a module collection</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="220"/>
+ <location filename="mainwindow.ui" line="250"/>
<source>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;
&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;
p, li { white-space: pre-wrap; }
@@ -1641,84 +1871,84 @@ p, li { white-space: pre-wrap; }
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="251"/>
+ <location filename="mainwindow.ui" line="281"/>
<source>Open list options...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="254"/>
+ <location filename="mainwindow.ui" line="284"/>
<source>Refresh list. This is usually not necessary unless you modified data outside the program.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="274"/>
+ <location filename="mainwindow.ui" line="304"/>
<source>Show Open Folders menu...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="288"/>
- <location filename="mainwindow.ui" line="802"/>
+ <location filename="mainwindow.ui" line="318"/>
+ <location filename="mainwindow.ui" line="819"/>
<source>Restore Backup...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="302"/>
- <location filename="mainwindow.ui" line="822"/>
- <location filename="mainwindow.cpp" line="4860"/>
+ <location filename="mainwindow.ui" line="332"/>
+ <location filename="mainwindow.ui" line="839"/>
+ <location filename="mainwindow.cpp" line="4726"/>
<source>Create Backup</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="316"/>
- <location filename="mainwindow.ui" line="836"/>
+ <location filename="mainwindow.ui" line="346"/>
+ <location filename="mainwindow.ui" line="853"/>
<source>Active:</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="329"/>
+ <location filename="mainwindow.ui" line="359"/>
<source>This provides statistics about the mod list. The total number of active mod is normally displayed. Other statistics may be accessed with the tooltip of this counter.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="356"/>
- <source>List of available mods.</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="mainwindow.ui" line="359"/>
+ <location filename="mainwindow.ui" line="386"/>
<source>This is a list of installed mods. Use the checkboxes to activate/deactivate mods and drag &amp; drop mods to change their &quot;installation&quot; orders.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="447"/>
- <location filename="mainwindow.ui" line="555"/>
- <location filename="mainwindow.ui" line="937"/>
- <location filename="mainwindow.ui" line="1273"/>
+ <location filename="mainwindow.ui" line="474"/>
+ <location filename="mainwindow.ui" line="575"/>
+ <location filename="mainwindow.ui" line="954"/>
+ <location filename="mainwindow.ui" line="1293"/>
<source>Filter</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="504"/>
+ <location filename="mainwindow.ui" line="524"/>
<source>Clear all Filters</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="531"/>
+ <location filename="mainwindow.ui" line="551"/>
<source>No groups</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="541"/>
+ <location filename="mainwindow.ui" line="556"/>
+ <source>Categories</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="mainwindow.ui" line="561"/>
<source>Nexus IDs</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="590"/>
+ <location filename="mainwindow.ui" line="610"/>
<source>Pick a program to run.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="593"/>
+ <location filename="mainwindow.ui" line="613"/>
<source>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;
&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;
p, li { white-space: pre-wrap; }
@@ -1728,12 +1958,12 @@ p, li { white-space: pre-wrap; }
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="641"/>
+ <location filename="mainwindow.ui" line="661"/>
<source>Run program</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="644"/>
+ <location filename="mainwindow.ui" line="664"/>
<source>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;
&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;
p, li { white-space: pre-wrap; }
@@ -1742,17 +1972,17 @@ p, li { white-space: pre-wrap; }
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="654"/>
+ <location filename="mainwindow.ui" line="674"/>
<source>Run</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="695"/>
+ <location filename="mainwindow.ui" line="715"/>
<source>Create a shortcut in your start menu or on the desktop to the specified program</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="698"/>
+ <location filename="mainwindow.ui" line="718"/>
<source>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;
&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;
p, li { white-space: pre-wrap; }
@@ -1761,32 +1991,32 @@ p, li { white-space: pre-wrap; }
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="705"/>
+ <location filename="mainwindow.ui" line="725"/>
<source>Shortcut</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="755"/>
+ <location filename="mainwindow.ui" line="775"/>
<source>Plugins</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="778"/>
+ <location filename="mainwindow.ui" line="795"/>
<source>Sort</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="849"/>
+ <location filename="mainwindow.ui" line="866"/>
<source>This provides statistics about the plugin list. The total number of active plugins is normally displayed. Other statistics may be accessed with the tooltip of this counter.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="876"/>
+ <location filename="mainwindow.ui" line="893"/>
<source>List of available esp/esm files</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="879"/>
+ <location filename="mainwindow.ui" line="896"/>
<source>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;
&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;
p, li { white-space: pre-wrap; }
@@ -1795,27 +2025,27 @@ p, li { white-space: pre-wrap; }
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="950"/>
+ <location filename="mainwindow.ui" line="967"/>
<source>Archives</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="970"/>
+ <location filename="mainwindow.ui" line="987"/>
<source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;BSAs / BA2s are bundles of game assets (textures, scripts, etc.). By default, the engine loads these bundles in a separate step from loose files. &lt;p&gt;Their load order is specified by the priority of the corresponding plugin (right pane, plugins tab).&lt;/p&gt;&lt;p&gt;If there is a matching plugin, the game will load them no matter what.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="973"/>
- <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Currently detected archives. (&lt;a href=&quot;#&quot;&gt;&lt;span style=&quot; text-decoration: underline; color:#0000ff;&quot;&gt;What is an archive?&lt;/span&gt;&lt;/a&gt;)&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <location filename="mainwindow.ui" line="990"/>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Currently detected archives. (&lt;a href=&quot;#&quot;&gt;What is an archive?&lt;/a&gt;)&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="988"/>
+ <location filename="mainwindow.ui" line="1005"/>
<source>List of available BS Archives. Archives not checked here are not managed by MO and ignore installation order.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="991"/>
+ <location filename="mainwindow.ui" line="1008"/>
<source>BSA files are archives (comparable to .zip files) that contain data assets (meshes, textures, ...) to be used by the game. As such they &quot;compete&quot; with loose files in your data directory over which is loaded.
By default, BSAs that share their base name with an enabled ESP (i.e. plugin.esp and plugin.bsa) are automatically loaded and will have precedence over all loose files, the installation order you set up to the left is then ignored!
@@ -1823,72 +2053,72 @@ p, li { white-space: pre-wrap; }
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1020"/>
+ <location filename="mainwindow.ui" line="1037"/>
<source>Data</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1038"/>
+ <location filename="mainwindow.ui" line="1055"/>
<source>refresh data-directory overview</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1041"/>
+ <location filename="mainwindow.ui" line="1058"/>
<source>Refresh the overview. This may take a moment.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1044"/>
- <location filename="mainwindow.ui" line="1187"/>
- <location filename="mainwindow.cpp" line="4733"/>
- <location filename="mainwindow.cpp" line="5551"/>
+ <location filename="mainwindow.ui" line="1061"/>
+ <location filename="mainwindow.ui" line="1207"/>
+ <location filename="mainwindow.cpp" line="4599"/>
+ <location filename="mainwindow.cpp" line="5580"/>
<source>Refresh</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1060"/>
+ <location filename="mainwindow.ui" line="1077"/>
<source>This is an overview of your data directory as visible to the game (and tools). </source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1073"/>
+ <location filename="mainwindow.ui" line="1090"/>
<source>File</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1078"/>
+ <location filename="mainwindow.ui" line="1095"/>
<source>Mod</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1090"/>
- <location filename="mainwindow.ui" line="1093"/>
+ <location filename="mainwindow.ui" line="1107"/>
+ <location filename="mainwindow.ui" line="1110"/>
<source>Filters the above list so that only conflicts are displayed.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1096"/>
+ <location filename="mainwindow.ui" line="1113"/>
<source>Show only conflicts</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1103"/>
- <location filename="mainwindow.ui" line="1109"/>
+ <location filename="mainwindow.ui" line="1120"/>
+ <location filename="mainwindow.ui" line="1126"/>
<source>Filters the above list so that files from archives are not shown</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1112"/>
+ <location filename="mainwindow.ui" line="1129"/>
<source>Show files from Archives</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1122"/>
+ <location filename="mainwindow.ui" line="1139"/>
<source>Saves</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1146"/>
+ <location filename="mainwindow.ui" line="1163"/>
<source>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;
&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;
p, li { white-space: pre-wrap; }
@@ -1899,22 +2129,22 @@ p, li { white-space: pre-wrap; }
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1166"/>
+ <location filename="mainwindow.ui" line="1186"/>
<source>Downloads</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1184"/>
+ <location filename="mainwindow.ui" line="1204"/>
<source>Refresh downloads view</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1215"/>
+ <location filename="mainwindow.ui" line="1235"/>
<source>This is a list of mods you downloaded from Nexus. Double click one to install it. You can also drag an archive into here.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1253"/>
+ <location filename="mainwindow.ui" line="1273"/>
<source>Show Hidden</source>
<translation type="unfinished"></translation>
</message>
@@ -1930,14 +2160,14 @@ p, li { white-space: pre-wrap; }
</message>
<message>
<location filename="mainwindow.ui" line="1367"/>
- <location filename="mainwindow.ui" line="1492"/>
+ <location filename="mainwindow.ui" line="1536"/>
<source>&amp;Tools</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="mainwindow.ui" line="1378"/>
- <location filename="mainwindow.ui" line="1588"/>
- <location filename="mainwindow.ui" line="1591"/>
+ <location filename="mainwindow.ui" line="1632"/>
+ <location filename="mainwindow.ui" line="1635"/>
<source>&amp;Help</source>
<translation type="unfinished"></translation>
</message>
@@ -1952,1128 +2182,1066 @@ p, li { white-space: pre-wrap; }
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1411"/>
+ <location filename="mainwindow.ui" line="1412"/>
<source>&amp;Run</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="mainwindow.ui" line="1426"/>
+ <location filename="mainwindow.ui" line="1774"/>
+ <source>Log</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="mainwindow.ui" line="1470"/>
<source>Install &amp;Mod...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1429"/>
+ <location filename="mainwindow.ui" line="1473"/>
<source>Install &amp;Mod</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1432"/>
- <location filename="mainwindow.ui" line="1435"/>
+ <location filename="mainwindow.ui" line="1476"/>
+ <location filename="mainwindow.ui" line="1479"/>
<source>Install a new mod from an archive</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1438"/>
+ <location filename="mainwindow.ui" line="1482"/>
<source>Ctrl+M</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1447"/>
+ <location filename="mainwindow.ui" line="1491"/>
<source>&amp;Profiles...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1450"/>
+ <location filename="mainwindow.ui" line="1494"/>
<source>&amp;Profiles</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1453"/>
- <location filename="mainwindow.ui" line="1456"/>
+ <location filename="mainwindow.ui" line="1497"/>
+ <location filename="mainwindow.ui" line="1500"/>
<source>Configure profiles</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1459"/>
+ <location filename="mainwindow.ui" line="1503"/>
<source>Ctrl+P</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1468"/>
+ <location filename="mainwindow.ui" line="1512"/>
<source>&amp;Executables...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1471"/>
+ <location filename="mainwindow.ui" line="1515"/>
<source>&amp;Executables</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1474"/>
- <location filename="mainwindow.ui" line="1477"/>
+ <location filename="mainwindow.ui" line="1518"/>
+ <location filename="mainwindow.ui" line="1521"/>
<source>Configure the executables that can be started through Mod Organizer</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1480"/>
+ <location filename="mainwindow.ui" line="1524"/>
<source>Ctrl+E</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1489"/>
+ <location filename="mainwindow.ui" line="1533"/>
<source>&amp;Tool Plugins</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1495"/>
+ <location filename="mainwindow.ui" line="1539"/>
<source>Tools</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1498"/>
+ <location filename="mainwindow.ui" line="1542"/>
<source>Ctrl+I</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1507"/>
+ <location filename="mainwindow.ui" line="1551"/>
<source>&amp;Settings...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1510"/>
+ <location filename="mainwindow.ui" line="1554"/>
<source>&amp;Settings</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1513"/>
- <location filename="mainwindow.ui" line="1516"/>
+ <location filename="mainwindow.ui" line="1557"/>
+ <location filename="mainwindow.ui" line="1560"/>
<source>Configure settings and workarounds</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1519"/>
+ <location filename="mainwindow.ui" line="1563"/>
<source>Ctrl+S</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1528"/>
- <location filename="mainwindow.ui" line="1531"/>
+ <location filename="mainwindow.ui" line="1572"/>
+ <location filename="mainwindow.ui" line="1575"/>
<source>Visit &amp;Nexus</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1534"/>
- <location filename="mainwindow.ui" line="1537"/>
+ <location filename="mainwindow.ui" line="1578"/>
+ <location filename="mainwindow.ui" line="1581"/>
<source>Visit the Nexus website in your browser for more mods</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1540"/>
+ <location filename="mainwindow.ui" line="1584"/>
<source>Ctrl+N</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1552"/>
- <location filename="mainwindow.ui" line="1555"/>
+ <location filename="mainwindow.ui" line="1596"/>
+ <location filename="mainwindow.ui" line="1599"/>
<source>&amp;Update Mod Organizer</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1558"/>
- <location filename="mainwindow.ui" line="1561"/>
+ <location filename="mainwindow.ui" line="1602"/>
+ <location filename="mainwindow.ui" line="1605"/>
<source>Mod Organizer is up-to-date</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1570"/>
+ <location filename="mainwindow.ui" line="1614"/>
<source>&amp;Notifications...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1573"/>
- <location filename="mainwindow.ui" line="1576"/>
+ <location filename="mainwindow.ui" line="1617"/>
+ <location filename="mainwindow.ui" line="1620"/>
<source>Open the notifications dialog</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1579"/>
+ <location filename="mainwindow.ui" line="1623"/>
<source>This button will be highlighted on the toolbar if MO discovered potential problems in your setup and provide tips on how to fix them.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1594"/>
- <location filename="mainwindow.ui" line="1597"/>
+ <location filename="mainwindow.ui" line="1638"/>
+ <location filename="mainwindow.ui" line="1641"/>
<source>Show help options</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1600"/>
+ <location filename="mainwindow.ui" line="1644"/>
<source>Ctrl+H</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1609"/>
- <location filename="mainwindow.ui" line="1612"/>
+ <location filename="mainwindow.ui" line="1653"/>
+ <location filename="mainwindow.ui" line="1656"/>
<source>&amp;Endorse ModOrganizer</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1615"/>
- <location filename="mainwindow.ui" line="1618"/>
- <location filename="mainwindow.cpp" line="5579"/>
+ <location filename="mainwindow.ui" line="1659"/>
+ <location filename="mainwindow.ui" line="1662"/>
+ <location filename="mainwindow.cpp" line="5606"/>
<source>Endorse Mod Organizer</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1623"/>
- <location filename="mainwindow.ui" line="1626"/>
- <source>Copy &amp;Log</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="mainwindow.ui" line="1629"/>
- <location filename="mainwindow.ui" line="1632"/>
- <source>Copy log to clipboard</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="mainwindow.ui" line="1641"/>
+ <location filename="mainwindow.ui" line="1671"/>
<source>&amp;Change Game...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1644"/>
+ <location filename="mainwindow.ui" line="1674"/>
<source>&amp;Change Game</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1647"/>
- <location filename="mainwindow.ui" line="1650"/>
+ <location filename="mainwindow.ui" line="1677"/>
+ <location filename="mainwindow.ui" line="1680"/>
<source>Open the Instance selection dialog to manage a different Game</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1655"/>
- <location filename="mainwindow.ui" line="1658"/>
+ <location filename="mainwindow.ui" line="1685"/>
+ <location filename="mainwindow.ui" line="1688"/>
<source>E&amp;xit</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1661"/>
- <location filename="mainwindow.ui" line="1664"/>
+ <location filename="mainwindow.ui" line="1691"/>
+ <location filename="mainwindow.ui" line="1694"/>
<source>Exits Mod Organizer</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1672"/>
+ <location filename="mainwindow.ui" line="1702"/>
<source>M&amp;ain Toolbar</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1680"/>
+ <location filename="mainwindow.ui" line="1710"/>
<source>&amp;Small Icons</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1688"/>
+ <location filename="mainwindow.ui" line="1718"/>
<source>Lar&amp;ge Icons</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1696"/>
+ <location filename="mainwindow.ui" line="1726"/>
<source>&amp;Icons Only</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1704"/>
+ <location filename="mainwindow.ui" line="1734"/>
<source>&amp;Text Only</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1712"/>
+ <location filename="mainwindow.ui" line="1742"/>
<source>I&amp;cons and Text</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1720"/>
+ <location filename="mainwindow.ui" line="1750"/>
<source>M&amp;edium Icons</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1728"/>
+ <location filename="mainwindow.ui" line="1758"/>
<source>&amp;Menu</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.ui" line="1736"/>
+ <location filename="mainwindow.ui" line="1766"/>
<source>Status &amp;bar</source>
- <oldsource>St&amp;atus bar</oldsource>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="341"/>
+ <location filename="mainwindow.cpp" line="311"/>
<source>Toolbar and Menu</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="342"/>
+ <location filename="mainwindow.cpp" line="312"/>
<source>Desktop</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="343"/>
+ <location filename="mainwindow.cpp" line="313"/>
<source>Start Menu</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="368"/>
+ <location filename="mainwindow.cpp" line="338"/>
<source>There is no supported sort mechanism for this game. You will probably have to use a third-party tool.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="578"/>
+ <location filename="mainwindow.cpp" line="628"/>
<source>Crash on exit</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="579"/>
+ <location filename="mainwindow.cpp" line="629"/>
<source>MO crashed while exiting. Some settings may not be saved.
Error: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="929"/>
+ <location filename="mainwindow.cpp" line="958"/>
<source>There are notifications to read</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="948"/>
+ <location filename="mainwindow.cpp" line="977"/>
<source>There are no notifications</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="1035"/>
- <location filename="mainwindow.cpp" line="4870"/>
- <location filename="mainwindow.cpp" line="4874"/>
+ <location filename="mainwindow.cpp" line="1064"/>
+ <location filename="mainwindow.cpp" line="4736"/>
+ <location filename="mainwindow.cpp" line="4740"/>
<source>Endorse</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="1039"/>
+ <location filename="mainwindow.cpp" line="1068"/>
<source>Won&apos;t Endorse</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="1056"/>
+ <location filename="mainwindow.cpp" line="1085"/>
<source>Help on UI</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="1060"/>
+ <location filename="mainwindow.cpp" line="1089"/>
<source>Documentation</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="1064"/>
+ <location filename="mainwindow.cpp" line="1093"/>
<source>Chat on Discord</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="1068"/>
+ <location filename="mainwindow.cpp" line="1097"/>
<source>Report Issue</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="1072"/>
+ <location filename="mainwindow.cpp" line="1101"/>
<source>Tutorials</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="1111"/>
+ <location filename="mainwindow.cpp" line="1140"/>
<source>About</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="1112"/>
+ <location filename="mainwindow.cpp" line="1141"/>
<source>About Qt</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="1171"/>
+ <location filename="mainwindow.cpp" line="1200"/>
<source>Name</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="1172"/>
+ <location filename="mainwindow.cpp" line="1201"/>
<source>Please enter a name for the new profile</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="1180"/>
+ <location filename="mainwindow.cpp" line="1209"/>
<source>failed to create profile: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="1234"/>
+ <location filename="mainwindow.cpp" line="1264"/>
<source>Show tutorial?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="1235"/>
+ <location filename="mainwindow.cpp" line="1265"/>
<source>You are starting Mod Organizer for the first time. Do you want to show a tutorial of its basic features? If you choose no you can always start the tutorial from the &quot;Help&quot;-menu.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="1278"/>
+ <location filename="mainwindow.cpp" line="1365"/>
<source>Downloads in progress</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="1279"/>
+ <location filename="mainwindow.cpp" line="1366"/>
<source>There are still downloads in progress, do you really want to quit?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="1403"/>
+ <location filename="mainwindow.cpp" line="1477"/>
<source>Plugin &quot;%1&quot; failed: %2</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="1405"/>
+ <location filename="mainwindow.cpp" line="1479"/>
<source>Plugin &quot;%1&quot; failed</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="1485"/>
+ <location filename="mainwindow.cpp" line="1559"/>
<source>Browse Mod Page</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="1713"/>
+ <location filename="mainwindow.cpp" line="1763"/>
<source>Also in: &lt;br&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="1724"/>
+ <location filename="mainwindow.cpp" line="1774"/>
<source>No conflict</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="1809"/>
+ <location filename="mainwindow.cpp" line="1878"/>
<source>&lt;Edit...&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="2087"/>
+ <location filename="mainwindow.cpp" line="1889"/>
+ <source>(no executables)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="mainwindow.cpp" line="2163"/>
<source>This bsa is enabled in the ini file so it may be required!</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="2149"/>
+ <location filename="mainwindow.cpp" line="2216"/>
<source>Activating Network Proxy</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="2252"/>
+ <location filename="mainwindow.cpp" line="2292"/>
<source>Notice: Your current MO version (%1) is lower than the previously used one (%2). The GUI may not downgrade gracefully, so you may experience oddities. However, there should be no serious issues.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="2365"/>
+ <location filename="mainwindow.cpp" line="2368"/>
<source>Choose Mod</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="2366"/>
+ <location filename="mainwindow.cpp" line="2369"/>
<source>Mod Archive</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="2473"/>
+ <location filename="mainwindow.cpp" line="2463"/>
<source>Start Tutorial?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="2474"/>
+ <location filename="mainwindow.cpp" line="2464"/>
<source>You&apos;re about to start a tutorial. For technical reasons it&apos;s not possible to end the tutorial early. Continue?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="2648"/>
- <source>failed to spawn notepad.exe: %1</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="mainwindow.cpp" line="2688"/>
+ <location filename="mainwindow.cpp" line="2670"/>
<source>failed to change origin name: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="2712"/>
+ <location filename="mainwindow.cpp" line="2694"/>
<source>failed to move &quot;%1&quot; from mod &quot;%2&quot; to &quot;%3&quot;: %4</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="2736"/>
- <source>&lt;Contains %1&gt;</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="mainwindow.cpp" line="2771"/>
- <source>&lt;Checked&gt;</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="mainwindow.cpp" line="2772"/>
- <source>&lt;Unchecked&gt;</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="mainwindow.cpp" line="2773"/>
- <source>&lt;Update&gt;</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="mainwindow.cpp" line="2774"/>
- <source>&lt;Mod Backup&gt;</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="mainwindow.cpp" line="2775"/>
- <source>&lt;Managed by MO&gt;</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="mainwindow.cpp" line="2776"/>
- <source>&lt;Managed outside MO&gt;</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="mainwindow.cpp" line="2777"/>
- <source>&lt;No category&gt;</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="mainwindow.cpp" line="2778"/>
- <source>&lt;Conflicted&gt;</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="mainwindow.cpp" line="2779"/>
- <source>&lt;Not Endorsed&gt;</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="mainwindow.cpp" line="2825"/>
+ <location filename="mainwindow.cpp" line="2707"/>
<source>failed to rename mod: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="2838"/>
+ <location filename="mainwindow.cpp" line="2720"/>
<source>Overwrite?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="2839"/>
+ <location filename="mainwindow.cpp" line="2721"/>
<source>This will replace the existing mod &quot;%1&quot;. Continue?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="2842"/>
+ <location filename="mainwindow.cpp" line="2724"/>
<source>failed to remove mod &quot;%1&quot;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="2846"/>
- <location filename="mainwindow.cpp" line="5379"/>
- <location filename="mainwindow.cpp" line="5403"/>
+ <location filename="mainwindow.cpp" line="2728"/>
+ <location filename="mainwindow.cpp" line="5246"/>
+ <location filename="mainwindow.cpp" line="5270"/>
<source>failed to rename &quot;%1&quot; to &quot;%2&quot;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="2934"/>
- <location filename="mainwindow.cpp" line="4437"/>
- <location filename="mainwindow.cpp" line="4445"/>
- <location filename="mainwindow.cpp" line="5004"/>
+ <location filename="mainwindow.cpp" line="2816"/>
+ <location filename="mainwindow.cpp" line="4302"/>
+ <location filename="mainwindow.cpp" line="4310"/>
+ <location filename="mainwindow.cpp" line="4836"/>
<source>Confirm</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="2935"/>
+ <location filename="mainwindow.cpp" line="2817"/>
<source>Remove the following mods?&lt;br&gt;&lt;ul&gt;%1&lt;/ul&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="2950"/>
+ <location filename="mainwindow.cpp" line="2832"/>
<source>failed to remove mod: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="2982"/>
- <location filename="mainwindow.cpp" line="2985"/>
- <location filename="mainwindow.cpp" line="2995"/>
+ <location filename="mainwindow.cpp" line="2864"/>
+ <location filename="mainwindow.cpp" line="2867"/>
+ <location filename="mainwindow.cpp" line="2877"/>
<source>Failed</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="2982"/>
+ <location filename="mainwindow.cpp" line="2864"/>
<source>Installation file no longer exists</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="2986"/>
+ <location filename="mainwindow.cpp" line="2868"/>
<source>Mods installed with old versions of MO can&apos;t be reinstalled in this way.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="2996"/>
+ <location filename="mainwindow.cpp" line="2878"/>
<source>Failed to create backup.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="3024"/>
+ <location filename="mainwindow.cpp" line="2906"/>
<source>Endorsing multiple mods will take a while. Please wait...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="3060"/>
+ <location filename="mainwindow.cpp" line="2942"/>
<source>Unendorsing multiple mods will take a while. Please wait...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="3149"/>
+ <location filename="mainwindow.cpp" line="3019"/>
<source>Failed to display overwrite dialog: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="3349"/>
+ <location filename="mainwindow.cpp" line="3210"/>
<source>Opening Nexus Links</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="3350"/>
+ <location filename="mainwindow.cpp" line="3211"/>
<source>You are trying to open %1 links to Nexus Mods. Are you sure you want to do this?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="3377"/>
+ <location filename="mainwindow.cpp" line="3238"/>
<source>Nexus ID for this mod is unknown</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="3388"/>
+ <location filename="mainwindow.cpp" line="3249"/>
<source>Opening Web Pages</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="3389"/>
+ <location filename="mainwindow.cpp" line="3250"/>
<source>You are trying to open %1 Web Pages. Are you sure you want to do this?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="3595"/>
+ <location filename="mainwindow.cpp" line="3456"/>
<source>&lt;table cellspacing=&quot;5&quot;&gt;&lt;tr&gt;&lt;th&gt;Type&lt;/th&gt;&lt;th&gt;All&lt;/th&gt;&lt;th&gt;Visible&lt;/th&gt;&lt;tr&gt;&lt;td&gt;Enabled mods:&amp;emsp;&lt;/td&gt;&lt;td align=right&gt;%1 / %2&lt;/td&gt;&lt;td align=right&gt;%3 / %4&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Unmanaged/DLCs:&amp;emsp;&lt;/td&gt;&lt;td align=right&gt;%5&lt;/td&gt;&lt;td align=right&gt;%6&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Mod backups:&amp;emsp;&lt;/td&gt;&lt;td align=right&gt;%7&lt;/td&gt;&lt;td align=right&gt;%8&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Separators:&amp;emsp;&lt;/td&gt;&lt;td align=right&gt;%9&lt;/td&gt;&lt;td align=right&gt;%10&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="3650"/>
+ <location filename="mainwindow.cpp" line="3511"/>
<source>&lt;table cellspacing=&quot;6&quot;&gt;&lt;tr&gt;&lt;th&gt;Type&lt;/th&gt;&lt;th&gt;Active &lt;/th&gt;&lt;th&gt;Total&lt;/th&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;All plugins:&lt;/td&gt;&lt;td align=right&gt;%1 &lt;/td&gt;&lt;td align=right&gt;%2&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;ESMs:&lt;/td&gt;&lt;td align=right&gt;%3 &lt;/td&gt;&lt;td align=right&gt;%4&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;ESPs:&lt;/td&gt;&lt;td align=right&gt;%7 &lt;/td&gt;&lt;td align=right&gt;%8&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;ESMs+ESPs:&lt;/td&gt;&lt;td align=right&gt;%9 &lt;/td&gt;&lt;td align=right&gt;%10&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;ESLs:&lt;/td&gt;&lt;td align=right&gt;%5 &lt;/td&gt;&lt;td align=right&gt;%6&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="3682"/>
- <location filename="mainwindow.cpp" line="3820"/>
- <location filename="mainwindow.cpp" line="4795"/>
+ <location filename="mainwindow.cpp" line="3543"/>
+ <location filename="mainwindow.cpp" line="3687"/>
+ <location filename="mainwindow.cpp" line="4661"/>
<source>Create Mod...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="3683"/>
+ <location filename="mainwindow.cpp" line="3544"/>
<source>This will create an empty mod.
Please enter a name:</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="3692"/>
- <location filename="mainwindow.cpp" line="3830"/>
+ <location filename="mainwindow.cpp" line="3553"/>
+ <location filename="mainwindow.cpp" line="3697"/>
<source>A mod with this name already exists</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="3720"/>
+ <location filename="mainwindow.cpp" line="3581"/>
<source>Create Separator...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="3721"/>
+ <location filename="mainwindow.cpp" line="3582"/>
<source>This will create a new separator.
Please enter a name:</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="3728"/>
+ <location filename="mainwindow.cpp" line="3589"/>
<source>A separator with this name already exists</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="3821"/>
+ <location filename="mainwindow.cpp" line="3688"/>
<source>This will move all files from overwrite into a new, regular mod.
Please enter a name:</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="3902"/>
+ <location filename="mainwindow.cpp" line="3761"/>
<source>Move successful.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="3923"/>
- <location filename="mainwindow.cpp" line="6141"/>
+ <location filename="mainwindow.cpp" line="3783"/>
+ <location filename="mainwindow.cpp" line="6237"/>
<source>Are you sure?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="3924"/>
+ <location filename="mainwindow.cpp" line="3784"/>
<source>About to recursively delete:
</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4319"/>
+ <location filename="mainwindow.cpp" line="4184"/>
<source>Continue?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4320"/>
+ <location filename="mainwindow.cpp" line="4185"/>
<source>The versioning scheme decides which version is considered newer than another.
This function will guess the versioning scheme under the assumption that the installed version is outdated.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4340"/>
+ <location filename="mainwindow.cpp" line="4205"/>
<source>Sorry</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4341"/>
+ <location filename="mainwindow.cpp" line="4206"/>
<source>I don&apos;t know a versioning scheme where %1 is newer than %2.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4437"/>
+ <location filename="mainwindow.cpp" line="4302"/>
<source>Really enable all visible mods?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4445"/>
+ <location filename="mainwindow.cpp" line="4310"/>
<source>Really disable all visible mods?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4522"/>
+ <location filename="mainwindow.cpp" line="4392"/>
<source>Export to csv</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4525"/>
+ <location filename="mainwindow.cpp" line="4395"/>
<source>CSV (Comma Separated Values) is a format that can be imported in programs like Excel to create a spreadsheet.
You can also use online editors and converters instead.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4528"/>
+ <location filename="mainwindow.cpp" line="4398"/>
<source>Select what mods you want export:</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4529"/>
+ <location filename="mainwindow.cpp" line="4399"/>
<source>All installed mods</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4530"/>
+ <location filename="mainwindow.cpp" line="4400"/>
<source>Only active (checked) mods from your current profile</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4531"/>
+ <location filename="mainwindow.cpp" line="4401"/>
<source>All currently visible mods in the mod list</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4552"/>
+ <location filename="mainwindow.cpp" line="4422"/>
<source>Choose what Columns to export:</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4555"/>
+ <location filename="mainwindow.cpp" line="4425"/>
<source>Mod_Priority</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4557"/>
+ <location filename="mainwindow.cpp" line="4427"/>
<source>Mod_Name</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4559"/>
+ <location filename="mainwindow.cpp" line="4429"/>
<source>Notes_column</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4560"/>
+ <location filename="mainwindow.cpp" line="4430"/>
<source>Mod_Status</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4562"/>
+ <location filename="mainwindow.cpp" line="4432"/>
<source>Primary_Category</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4563"/>
+ <location filename="mainwindow.cpp" line="4433"/>
<source>Nexus_ID</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4564"/>
+ <location filename="mainwindow.cpp" line="4434"/>
<source>Mod_Nexus_URL</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4565"/>
+ <location filename="mainwindow.cpp" line="4435"/>
<source>Mod_Version</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4566"/>
+ <location filename="mainwindow.cpp" line="4436"/>
<source>Install_Date</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4567"/>
+ <location filename="mainwindow.cpp" line="4437"/>
<source>Download_File_Name</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4675"/>
+ <location filename="mainwindow.cpp" line="4545"/>
<source>export failed: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4694"/>
+ <location filename="mainwindow.cpp" line="4564"/>
<source>Open Game folder</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4696"/>
+ <location filename="mainwindow.cpp" line="4565"/>
<source>Open MyGames folder</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4698"/>
+ <location filename="mainwindow.cpp" line="4566"/>
<source>Open INIs folder</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4702"/>
+ <location filename="mainwindow.cpp" line="4571"/>
<source>Open Instance folder</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4704"/>
+ <location filename="mainwindow.cpp" line="4572"/>
<source>Open Mods folder</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4706"/>
+ <location filename="mainwindow.cpp" line="4573"/>
<source>Open Profile folder</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4708"/>
+ <location filename="mainwindow.cpp" line="4574"/>
<source>Open Downloads folder</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4712"/>
+ <location filename="mainwindow.cpp" line="4580"/>
<source>Open MO2 Install folder</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4714"/>
+ <location filename="mainwindow.cpp" line="4581"/>
<source>Open MO2 Plugins folder</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4716"/>
+ <location filename="mainwindow.cpp" line="4582"/>
+ <source>Open MO2 Stylesheets folder</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="mainwindow.cpp" line="4583"/>
<source>Open MO2 Logs folder</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4724"/>
+ <location filename="mainwindow.cpp" line="4590"/>
<source>Install Mod...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4725"/>
+ <location filename="mainwindow.cpp" line="4591"/>
<source>Create empty mod</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4726"/>
+ <location filename="mainwindow.cpp" line="4592"/>
<source>Create Separator</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4730"/>
+ <location filename="mainwindow.cpp" line="4596"/>
<source>Enable all visible</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4731"/>
+ <location filename="mainwindow.cpp" line="4597"/>
<source>Disable all visible</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4732"/>
+ <location filename="mainwindow.cpp" line="4598"/>
<source>Check for updates</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4734"/>
+ <location filename="mainwindow.cpp" line="4600"/>
<source>Export to csv...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4743"/>
- <location filename="mainwindow.cpp" line="4759"/>
+ <location filename="mainwindow.cpp" line="4609"/>
+ <location filename="mainwindow.cpp" line="4625"/>
<source>Send to</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4744"/>
- <location filename="mainwindow.cpp" line="4760"/>
+ <location filename="mainwindow.cpp" line="4610"/>
+ <location filename="mainwindow.cpp" line="4626"/>
<source>Top</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4745"/>
- <location filename="mainwindow.cpp" line="4761"/>
+ <location filename="mainwindow.cpp" line="4611"/>
+ <location filename="mainwindow.cpp" line="4627"/>
<source>Bottom</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4746"/>
- <location filename="mainwindow.cpp" line="4762"/>
+ <location filename="mainwindow.cpp" line="4612"/>
+ <location filename="mainwindow.cpp" line="4628"/>
<source>Priority...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4747"/>
+ <location filename="mainwindow.cpp" line="4613"/>
<source>Separator...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4786"/>
+ <location filename="mainwindow.cpp" line="4652"/>
<source>All Mods</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4794"/>
+ <location filename="mainwindow.cpp" line="4660"/>
<source>Sync to Mods...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4796"/>
+ <location filename="mainwindow.cpp" line="4662"/>
<source>Move content to Mod...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4797"/>
+ <location filename="mainwindow.cpp" line="4663"/>
<source>Clear Overwrite...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4799"/>
- <location filename="mainwindow.cpp" line="4922"/>
+ <location filename="mainwindow.cpp" line="4665"/>
+ <location filename="mainwindow.cpp" line="4788"/>
<source>Open in Explorer</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4801"/>
+ <location filename="mainwindow.cpp" line="4667"/>
<source>Restore Backup</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4802"/>
+ <location filename="mainwindow.cpp" line="4668"/>
<source>Remove Backup...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4805"/>
- <location filename="mainwindow.cpp" line="4824"/>
+ <location filename="mainwindow.cpp" line="4671"/>
+ <location filename="mainwindow.cpp" line="4690"/>
<source>Change Categories</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4809"/>
- <location filename="mainwindow.cpp" line="4829"/>
+ <location filename="mainwindow.cpp" line="4675"/>
+ <location filename="mainwindow.cpp" line="4695"/>
<source>Primary Category</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4813"/>
+ <location filename="mainwindow.cpp" line="4679"/>
<source>Rename Separator...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4814"/>
+ <location filename="mainwindow.cpp" line="4680"/>
<source>Remove Separator...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4817"/>
+ <location filename="mainwindow.cpp" line="4683"/>
<source>Select Color...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4819"/>
+ <location filename="mainwindow.cpp" line="4685"/>
<source>Reset Color</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4836"/>
+ <location filename="mainwindow.cpp" line="4702"/>
<source>Change versioning scheme</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4840"/>
+ <location filename="mainwindow.cpp" line="4706"/>
<source>Force-check updates</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4842"/>
+ <location filename="mainwindow.cpp" line="4708"/>
<source>Un-ignore update</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4845"/>
+ <location filename="mainwindow.cpp" line="4711"/>
<source>Ignore update</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4850"/>
- <location filename="mainwindow.cpp" line="6265"/>
+ <location filename="mainwindow.cpp" line="4716"/>
+ <location filename="mainwindow.cpp" line="6395"/>
<source>Enable selected</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4851"/>
- <location filename="mainwindow.cpp" line="6266"/>
+ <location filename="mainwindow.cpp" line="4717"/>
+ <location filename="mainwindow.cpp" line="6396"/>
<source>Disable selected</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4857"/>
+ <location filename="mainwindow.cpp" line="4723"/>
<source>Rename Mod...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4858"/>
+ <location filename="mainwindow.cpp" line="4724"/>
<source>Reinstall Mod</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4859"/>
+ <location filename="mainwindow.cpp" line="4725"/>
<source>Remove Mod...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4867"/>
+ <location filename="mainwindow.cpp" line="4733"/>
<source>Un-Endorse</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4871"/>
+ <location filename="mainwindow.cpp" line="4737"/>
<source>Won&apos;t endorse</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4877"/>
+ <location filename="mainwindow.cpp" line="4743"/>
<source>Endorsement state unknown</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4887"/>
+ <location filename="mainwindow.cpp" line="4753"/>
<source>Start tracking</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4890"/>
+ <location filename="mainwindow.cpp" line="4756"/>
<source>Stop tracking</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4893"/>
+ <location filename="mainwindow.cpp" line="4759"/>
<source>Tracked state unknown</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4904"/>
+ <location filename="mainwindow.cpp" line="4770"/>
<source>Ignore missing data</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4908"/>
+ <location filename="mainwindow.cpp" line="4774"/>
<source>Mark as converted/working</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4912"/>
+ <location filename="mainwindow.cpp" line="4778"/>
<source>Visit on Nexus</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4918"/>
+ <location filename="mainwindow.cpp" line="4784"/>
<source>Visit on %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4926"/>
+ <location filename="mainwindow.cpp" line="4792"/>
<source>Information...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4933"/>
- <location filename="mainwindow.cpp" line="6318"/>
+ <location filename="mainwindow.cpp" line="4799"/>
+ <location filename="mainwindow.cpp" line="6448"/>
<source>Exception: </source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4935"/>
- <location filename="mainwindow.cpp" line="6320"/>
+ <location filename="mainwindow.cpp" line="4801"/>
+ <location filename="mainwindow.cpp" line="6450"/>
<source>Unknown exception</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4964"/>
- <source>&lt;All&gt;</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="mainwindow.cpp" line="4966"/>
- <source>&lt;Multiple&gt;</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="mainwindow.cpp" line="5001"/>
+ <location filename="mainwindow.cpp" line="4833"/>
<source>%1 more</source>
<translation type="unfinished"></translation>
</message>
<message numerus="yes">
- <location filename="mainwindow.cpp" line="5005"/>
+ <location filename="mainwindow.cpp" line="4837"/>
<source>Are you sure you want to remove the following %n save(s)?&lt;br&gt;&lt;ul&gt;%1&lt;/ul&gt;&lt;br&gt;Removed saves will be sent to the Recycle Bin.</source>
<translation type="unfinished">
<numerusform></numerusform>
@@ -3081,12 +3249,12 @@ You can also use online editors and converters instead.</source>
</translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5050"/>
+ <location filename="mainwindow.cpp" line="4882"/>
<source>Enable Mods...</source>
<translation type="unfinished"></translation>
</message>
<message numerus="yes">
- <location filename="mainwindow.cpp" line="5065"/>
+ <location filename="mainwindow.cpp" line="4897"/>
<source>Delete %n save(s)</source>
<translation type="unfinished">
<numerusform></numerusform>
@@ -3094,361 +3262,335 @@ You can also use online editors and converters instead.</source>
</translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5124"/>
- <source>Restarting MO</source>
+ <location filename="mainwindow.cpp" line="4977"/>
+ <source>Restart Mod Organizer</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5125"/>
- <source>Changing the managed game directory requires restarting MO.
-Any pending downloads will be paused.
-
-Click OK to restart MO now.</source>
+ <location filename="mainwindow.cpp" line="4979"/>
+ <source>Mod Organizer must restart to finish configuration changes</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5145"/>
+ <location filename="mainwindow.cpp" line="4981"/>
+ <source>Restart</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="mainwindow.cpp" line="4982"/>
+ <source>Continue</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="mainwindow.cpp" line="4982"/>
+ <source>Some things might be weird.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="mainwindow.cpp" line="5003"/>
<source>Can&apos;t change download directory while downloads are in progress!</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5292"/>
+ <location filename="mainwindow.cpp" line="5160"/>
<source>failed to write to file %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5298"/>
+ <location filename="mainwindow.cpp" line="5166"/>
<source>%1 written</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5322"/>
+ <location filename="mainwindow.cpp" line="5184"/>
<source>Enter Name</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5323"/>
- <source>Please enter a name for the executable</source>
+ <location filename="mainwindow.cpp" line="5185"/>
+ <source>Enter a name for the executable</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5342"/>
+ <location filename="mainwindow.cpp" line="5207"/>
<source>Not an executable</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5342"/>
+ <location filename="mainwindow.cpp" line="5208"/>
<source>This is not a recognized executable.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5364"/>
- <location filename="mainwindow.cpp" line="5389"/>
+ <location filename="mainwindow.cpp" line="5231"/>
+ <location filename="mainwindow.cpp" line="5256"/>
<source>Replace file?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5364"/>
+ <location filename="mainwindow.cpp" line="5231"/>
<source>There already is a hidden version of this file. Replace it?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5367"/>
- <location filename="mainwindow.cpp" line="5392"/>
+ <location filename="mainwindow.cpp" line="5234"/>
+ <location filename="mainwindow.cpp" line="5259"/>
<source>File operation failed</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5367"/>
- <location filename="mainwindow.cpp" line="5392"/>
+ <location filename="mainwindow.cpp" line="5234"/>
+ <location filename="mainwindow.cpp" line="5259"/>
<source>Failed to remove &quot;%1&quot;. Maybe you lack the required file permissions?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5389"/>
+ <location filename="mainwindow.cpp" line="5256"/>
<source>There already is a visible version of this file. Replace it?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5433"/>
- <location filename="mainwindow.cpp" line="6928"/>
+ <location filename="mainwindow.cpp" line="5300"/>
+ <location filename="mainwindow.cpp" line="6787"/>
<source>Set Priority</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5433"/>
+ <location filename="mainwindow.cpp" line="5300"/>
<source>Set the priority of the selected plugins</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5497"/>
+ <location filename="mainwindow.cpp" line="5471"/>
<source>Update available</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5524"/>
- <source>Open/Execute</source>
+ <location filename="mainwindow.cpp" line="5506"/>
+ <source>&amp;Execute</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5525"/>
- <source>Add as Executable</source>
+ <location filename="mainwindow.cpp" line="5507"/>
+ <source>Execute with &amp;VFS</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5529"/>
+ <location filename="mainwindow.cpp" line="5509"/>
+ <source>&amp;Open</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="mainwindow.cpp" line="5510"/>
+ <source>Open with &amp;VFS</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="mainwindow.cpp" line="5514"/>
<source>Preview</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5542"/>
+ <location filename="mainwindow.cpp" line="5551"/>
+ <source>&amp;Add as Executable</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="mainwindow.cpp" line="5564"/>
<source>Un-Hide</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5544"/>
+ <location filename="mainwindow.cpp" line="5566"/>
<source>Hide</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5550"/>
+ <location filename="mainwindow.cpp" line="5579"/>
<source>Write To File...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5580"/>
+ <location filename="mainwindow.cpp" line="5607"/>
<source>Do you want to endorse Mod Organizer on %1 now?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5594"/>
+ <location filename="mainwindow.cpp" line="5621"/>
<source>Abstain from Endorsing Mod Organizer</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5595"/>
+ <location filename="mainwindow.cpp" line="5622"/>
<source>Are you sure you want to abstain from endorsing Mod Organizer 2?
You will have to visit the mod page on the %1 Nexus site to change your mind.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5675"/>
- <location filename="mainwindow.cpp" line="5676"/>
+ <location filename="mainwindow.cpp" line="5713"/>
<source>Thank you for endorsing MO2! :)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5678"/>
- <location filename="mainwindow.cpp" line="5679"/>
+ <location filename="mainwindow.cpp" line="5719"/>
<source>Please reconsider endorsing MO2 on Nexus!</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5914"/>
+ <location filename="mainwindow.cpp" line="5983"/>
<source>Thank you!</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5914"/>
+ <location filename="mainwindow.cpp" line="5983"/>
<source>Thank you for your endorsement!</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5917"/>
- <source>Okay.</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="mainwindow.cpp" line="5917"/>
- <source>This mod will not be endorsed and will no longer ask you to endorse.</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="mainwindow.cpp" line="5976"/>
+ <location filename="mainwindow.cpp" line="6075"/>
<source>Mod ID %1 no longer seems to be available on Nexus.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5978"/>
+ <location filename="mainwindow.cpp" line="6077"/>
<source>Request to Nexus failed: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5994"/>
- <location filename="mainwindow.cpp" line="6056"/>
+ <location filename="mainwindow.cpp" line="6093"/>
+ <location filename="mainwindow.cpp" line="6155"/>
<source>failed to read %1: %2</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="6006"/>
- <location filename="mainwindow.cpp" line="6493"/>
+ <location filename="mainwindow.cpp" line="6105"/>
<source>Error</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="6006"/>
+ <location filename="mainwindow.cpp" line="6105"/>
<source>failed to extract %1 (errorcode %2)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="6038"/>
+ <location filename="mainwindow.cpp" line="6137"/>
<source>Extract BSA</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="6067"/>
+ <location filename="mainwindow.cpp" line="6166"/>
<source>This archive contains invalid hashes. Some files may be broken.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="6113"/>
+ <location filename="mainwindow.cpp" line="6212"/>
<source>Extract...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="6142"/>
+ <location filename="mainwindow.cpp" line="6237"/>
<source>This will restart MO, continue?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="6189"/>
- <source>Edit Categories...</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="mainwindow.cpp" line="6190"/>
- <source>Deselect filter</source>
+ <location filename="mainwindow.cpp" line="6315"/>
+ <source>&lt;Multiple&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="6249"/>
+ <location filename="mainwindow.cpp" line="6379"/>
<source>Remove &apos;%1&apos; from the toolbar</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="6270"/>
+ <location filename="mainwindow.cpp" line="6400"/>
<source>Enable all</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="6271"/>
+ <location filename="mainwindow.cpp" line="6401"/>
<source>Disable all</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="6292"/>
+ <location filename="mainwindow.cpp" line="6422"/>
<source>Unlock load order</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="6295"/>
+ <location filename="mainwindow.cpp" line="6425"/>
<source>Lock load order</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="6305"/>
+ <location filename="mainwindow.cpp" line="6435"/>
<source>Open Origin in Explorer</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="6310"/>
+ <location filename="mainwindow.cpp" line="6440"/>
<source>Open Origin Info...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="6439"/>
- <source>depends on missing &quot;%1&quot;</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="mainwindow.cpp" line="6443"/>
- <source>incompatible with &quot;%1&quot;</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="mainwindow.cpp" line="6469"/>
- <source>Please wait while LOOT is running</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="mainwindow.cpp" line="6566"/>
- <source>loot failed. Exit code was: %1</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="mainwindow.cpp" line="6588"/>
- <source>failed to start loot</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="mainwindow.cpp" line="6591"/>
- <source>failed to run loot: %1</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="mainwindow.cpp" line="6595"/>
- <source>Errors occurred</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="mainwindow.cpp" line="6642"/>
+ <location filename="mainwindow.cpp" line="6548"/>
<source>Backup of load order created</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="6652"/>
+ <location filename="mainwindow.cpp" line="6558"/>
<source>Choose backup to restore</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="6665"/>
+ <location filename="mainwindow.cpp" line="6571"/>
<source>No Backups</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="6665"/>
+ <location filename="mainwindow.cpp" line="6571"/>
<source>There are no backups to restore</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="6686"/>
- <location filename="mainwindow.cpp" line="6708"/>
+ <location filename="mainwindow.cpp" line="6596"/>
+ <location filename="mainwindow.cpp" line="6621"/>
<source>Restore failed</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="6687"/>
- <location filename="mainwindow.cpp" line="6709"/>
+ <location filename="mainwindow.cpp" line="6597"/>
+ <location filename="mainwindow.cpp" line="6622"/>
<source>Failed to restore the backup. Errorcode: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="6698"/>
+ <location filename="mainwindow.cpp" line="6609"/>
<source>Backup of mod list created</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="6804"/>
+ <location filename="mainwindow.cpp" line="6692"/>
<source>A file with the same name has already been downloaded. What would you like to do?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="6806"/>
+ <location filename="mainwindow.cpp" line="6694"/>
<source>Overwrite</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="6807"/>
+ <location filename="mainwindow.cpp" line="6695"/>
<source>Rename new file</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="6808"/>
+ <location filename="mainwindow.cpp" line="6696"/>
<source>Ignore file</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="6928"/>
+ <location filename="mainwindow.cpp" line="6787"/>
<source>Set the priority of the selected mods</source>
<translation type="unfinished"></translation>
</message>
@@ -3465,101 +3607,100 @@ You will have to visit the mod page on the %1 Nexus site to change your mind.</s
<context>
<name>ModInfo</name>
<message>
- <location filename="modinfo.cpp" line="99"/>
+ <location filename="modinfo.cpp" line="114"/>
<source>Plugins</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfo.cpp" line="100"/>
+ <location filename="modinfo.cpp" line="115"/>
<source>Textures</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfo.cpp" line="101"/>
+ <location filename="modinfo.cpp" line="116"/>
<source>Meshes</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfo.cpp" line="102"/>
+ <location filename="modinfo.cpp" line="117"/>
<source>Bethesda Archive</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfo.cpp" line="103"/>
+ <location filename="modinfo.cpp" line="118"/>
<source>UI Changes</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfo.cpp" line="104"/>
+ <location filename="modinfo.cpp" line="119"/>
<source>Sound Effects</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfo.cpp" line="105"/>
+ <location filename="modinfo.cpp" line="120"/>
<source>Scripts</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfo.cpp" line="106"/>
+ <location filename="modinfo.cpp" line="121"/>
<source>Script Extender</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfo.cpp" line="107"/>
+ <location filename="modinfo.cpp" line="122"/>
<source>Script Extender Files</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfo.cpp" line="108"/>
+ <location filename="modinfo.cpp" line="123"/>
<source>SkyProc Tools</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfo.cpp" line="109"/>
+ <location filename="modinfo.cpp" line="124"/>
<source>MCM Data</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfo.cpp" line="110"/>
+ <location filename="modinfo.cpp" line="125"/>
<source>INI files</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfo.cpp" line="111"/>
+ <location filename="modinfo.cpp" line="126"/>
<source>ModGroup files</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfo.cpp" line="113"/>
+ <location filename="modinfo.cpp" line="128"/>
<source>invalid content type: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfo.cpp" line="137"/>
+ <location filename="modinfo.cpp" line="152"/>
<source>invalid mod index: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfo.cpp" line="182"/>
+ <location filename="modinfo.cpp" line="197"/>
<source>remove: invalid mod index %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfo.cpp" line="322"/>
+ <location filename="modinfo.cpp" line="337"/>
<source>All of your mods have been checked recently. We restrict update checks to help preserve your available API requests.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfo.cpp" line="325"/>
+ <location filename="modinfo.cpp" line="340"/>
<source>You have mods that haven&apos;t been checked within the last month using the new API. These mods must be checked before we can use the bulk update API. This will consume significantly more API requests than usual. You will need to rerun the update check once complete in order to parse the remaining mods.</source>
- <oldsource>You have mods that haven&apos;t been checked within 30 days using the new API. These mods must be checked before we can use the bulk update API. This will consume significantly more API requests than usual. You will need to rerun the update check once complete in order to parse the remaining mods.</oldsource>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>ModInfoBackup</name>
<message>
- <location filename="modinfobackup.cpp" line="13"/>
+ <location filename="modinfobackup.cpp" line="14"/>
<source>This is the backup of a mod</source>
<translation type="unfinished"></translation>
</message>
@@ -3619,27 +3760,32 @@ You will have to visit the mod page on the %1 Nexus site to change your mind.</s
</message>
<message>
<location filename="modinfodialog.ui" line="273"/>
+ <source>Open with Preview Plugin</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="modinfodialog.ui" line="280"/>
<source>Open in Explorer</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.ui" line="287"/>
+ <location filename="modinfodialog.ui" line="294"/>
<source>0x0</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.ui" line="305"/>
- <location filename="modinfodialog.ui" line="330"/>
+ <location filename="modinfodialog.ui" line="312"/>
+ <location filename="modinfodialog.ui" line="337"/>
<source>Optional ESPs</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.ui" line="337"/>
+ <location filename="modinfodialog.ui" line="344"/>
<source>List of esps, esms, and esls that can not be loaded by the game.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.ui" line="340"/>
+ <location filename="modinfodialog.ui" line="347"/>
<source>List of esps, esms, and esls contained in this plugin that currently can not be loaded by the game. They will not even appear in the esp-list in the main MO-window.
They usually contain optional functionality, see the readme.
@@ -3647,132 +3793,132 @@ Most mods do not have optional esps, so chances are good you are looking at an e
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.ui" line="384"/>
+ <location filename="modinfodialog.ui" line="391"/>
<source>Move a file to the data directory.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.ui" line="387"/>
- <source>This moves a esp to the esp directory so it can be enabled in the main window. Please note that the ESP merely becomes &quot;available&quot;, it will not necessarily be loaded! That is configured in the main window of omo.</source>
+ <location filename="modinfodialog.ui" line="394"/>
+ <source>This moves a esp to the esp directory so it can be enabled in the main window. Please note that the ESP merely becomes &quot;available&quot;, it will not necessarily be loaded! That is configured in the main window of MO.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.ui" line="407"/>
- <source>Make the selected mod in the lower list unavailable.</source>
+ <location filename="modinfodialog.ui" line="414"/>
+ <source>Make the selected mod in the right list unavailable.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.ui" line="410"/>
- <source>The selected esp (in the lower list) will be pushed into a subdirectory of the mod and will thus become &quot;invisible&quot; to the game. It can then no longer be activated.</source>
+ <location filename="modinfodialog.ui" line="417"/>
+ <source>The selected esp (in the right list) will be pushed into a subdirectory of the mod and will thus become &quot;invisible&quot; to the game. It can then no longer be activated.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.ui" line="460"/>
+ <location filename="modinfodialog.ui" line="467"/>
<source>Available ESPs</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.ui" line="467"/>
+ <location filename="modinfodialog.ui" line="474"/>
<source>ESPs in the data directory and thus visible to the game.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.ui" line="470"/>
+ <location filename="modinfodialog.ui" line="477"/>
<source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;These are the mod files that are in the (virtual) data directory of your game and will thus be selectable in the esp list in the main window.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.ui" line="488"/>
+ <location filename="modinfodialog.ui" line="495"/>
<source>Conflicts</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.ui" line="498"/>
+ <location filename="modinfodialog.ui" line="505"/>
<source>General</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.ui" line="539"/>
+ <location filename="modinfodialog.ui" line="546"/>
<source>The following conflicted files are provided by this mod</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.ui" line="596"/>
+ <location filename="modinfodialog.ui" line="603"/>
<source>The following conflicted files are provided by other mods</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.ui" line="650"/>
+ <location filename="modinfodialog.ui" line="657"/>
<source>The following files have no conflicts</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.ui" line="707"/>
+ <location filename="modinfodialog.ui" line="714"/>
<source>Advanced</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.ui" line="777"/>
+ <location filename="modinfodialog.ui" line="784"/>
<source>Whether files that have no conflicts should be visible in the list</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.ui" line="780"/>
+ <location filename="modinfodialog.ui" line="787"/>
<source>Show files that have no conflicts</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.ui" line="790"/>
+ <location filename="modinfodialog.ui" line="797"/>
<source>Shows all mods overwriting or being overwritten by this mod</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.ui" line="793"/>
+ <location filename="modinfodialog.ui" line="800"/>
<source>Show all conflicting mods</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.ui" line="803"/>
+ <location filename="modinfodialog.ui" line="810"/>
<source>Shows only the nearest conflicting mods, in order of priority</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.ui" line="806"/>
+ <location filename="modinfodialog.ui" line="813"/>
<source>Show nearest conflicting mod</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.ui" line="828"/>
+ <location filename="modinfodialog.ui" line="835"/>
<source>Filter</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.ui" line="849"/>
+ <location filename="modinfodialog.ui" line="856"/>
<source>Categories</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.ui" line="869"/>
+ <location filename="modinfodialog.ui" line="876"/>
<source>Primary Category</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.ui" line="886"/>
+ <location filename="modinfodialog.ui" line="893"/>
<source>Nexus Info</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.ui" line="909"/>
+ <location filename="modinfodialog.ui" line="916"/>
<source>Mod ID</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.ui" line="916"/>
+ <location filename="modinfodialog.ui" line="923"/>
<source>Mod ID for this mod on Nexus.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.ui" line="919"/>
+ <location filename="modinfodialog.ui" line="926"/>
<source>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;
&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;
p, li { white-space: pre-wrap; }
@@ -3781,22 +3927,22 @@ p, li { white-space: pre-wrap; }
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.ui" line="930"/>
+ <location filename="modinfodialog.ui" line="937"/>
<source>Source Game</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.ui" line="937"/>
+ <location filename="modinfodialog.ui" line="944"/>
<source>Source game for this mod.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.ui" line="940"/>
+ <location filename="modinfodialog.ui" line="947"/>
<source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Source game for this mod. This determines where the mod was downloaded from and decides where to fetch info, version updates, and send endorsements. Changing this will likely require you to enter a new Mod ID.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.ui" line="947"/>
+ <location filename="modinfodialog.ui" line="954"/>
<source>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;
&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;
p, li { white-space: pre-wrap; }
@@ -3805,83 +3951,83 @@ p, li { white-space: pre-wrap; }
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.ui" line="954"/>
+ <location filename="modinfodialog.ui" line="961"/>
<source>Version</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.ui" line="1004"/>
- <location filename="modinfodialog.ui" line="1010"/>
+ <location filename="modinfodialog.ui" line="1011"/>
+ <location filename="modinfodialog.ui" line="1017"/>
<source>Refresh</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.ui" line="1007"/>
+ <location filename="modinfodialog.ui" line="1014"/>
<source>Refresh all information from Nexus.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.ui" line="1021"/>
- <location filename="modinfodialog.ui" line="1146"/>
+ <location filename="modinfodialog.ui" line="1028"/>
+ <location filename="modinfodialog.ui" line="1153"/>
<source>Open in Browser</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.ui" line="1032"/>
+ <location filename="modinfodialog.ui" line="1039"/>
<source>Endorse</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.ui" line="1043"/>
+ <location filename="modinfodialog.ui" line="1050"/>
<source>Track</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.ui" line="1110"/>
+ <location filename="modinfodialog.ui" line="1117"/>
<source>about:blank</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.ui" line="1136"/>
+ <location filename="modinfodialog.ui" line="1143"/>
<source>Use Custom URL</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.ui" line="1157"/>
+ <location filename="modinfodialog.ui" line="1164"/>
<source>Notes</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.ui" line="1163"/>
- <location filename="modinfodialog.ui" line="1166"/>
- <location filename="modinfodialog.ui" line="1169"/>
+ <location filename="modinfodialog.ui" line="1170"/>
+ <location filename="modinfodialog.ui" line="1173"/>
+ <location filename="modinfodialog.ui" line="1176"/>
<source>Enter comments about the mod here. These are displayed in the notes column of the mod list.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.ui" line="1176"/>
- <location filename="modinfodialog.ui" line="1179"/>
- <location filename="modinfodialog.ui" line="1185"/>
+ <location filename="modinfodialog.ui" line="1183"/>
+ <location filename="modinfodialog.ui" line="1186"/>
+ <location filename="modinfodialog.ui" line="1192"/>
<source>Enter notes about the mod here. These can be viewed in the mod list by hovering over the notes column or the flags column.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.ui" line="1193"/>
+ <location filename="modinfodialog.ui" line="1200"/>
<source>Filetree</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.ui" line="1207"/>
+ <location filename="modinfodialog.ui" line="1214"/>
<source>Open Mod in Explorer</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.ui" line="1232"/>
+ <location filename="modinfodialog.ui" line="1239"/>
<source>A directory view of this mod</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.ui" line="1235"/>
+ <location filename="modinfodialog.ui" line="1242"/>
<source>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;
&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;
p, li { white-space: pre-wrap; }
@@ -3891,17 +4037,17 @@ p, li { white-space: pre-wrap; }
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.ui" line="1265"/>
+ <location filename="modinfodialog.ui" line="1275"/>
<source>Previous</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.ui" line="1275"/>
+ <location filename="modinfodialog.ui" line="1285"/>
<source>Next</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialog.ui" line="1298"/>
+ <location filename="modinfodialog.ui" line="1308"/>
<source>Close</source>
<translation type="unfinished"></translation>
</message>
@@ -3909,22 +4055,22 @@ p, li { white-space: pre-wrap; }
<context>
<name>ModInfoForeign</name>
<message>
- <location filename="modinfoforeign.cpp" line="47"/>
+ <location filename="modinfoforeign.cpp" line="42"/>
<source>This pseudo mod represents content managed outside MO. It isn&apos;t modified by MO.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfoforeign.cpp" line="61"/>
+ <location filename="modinfoforeign.cpp" line="56"/>
<source>DLC: </source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfoforeign.cpp" line="64"/>
+ <location filename="modinfoforeign.cpp" line="60"/>
<source>Creation Club: </source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfoforeign.cpp" line="67"/>
+ <location filename="modinfoforeign.cpp" line="64"/>
<source>Unmanaged: </source>
<translation type="unfinished"></translation>
</message>
@@ -3932,7 +4078,7 @@ p, li { white-space: pre-wrap; }
<context>
<name>ModInfoOverwrite</name>
<message>
- <location filename="modinfooverwrite.cpp" line="51"/>
+ <location filename="modinfooverwrite.cpp" line="60"/>
<source>This pseudo mod contains files from the virtual data tree that got modified (i.e. by the construction kit)</source>
<translation type="unfinished"></translation>
</message>
@@ -3940,12 +4086,12 @@ p, li { white-space: pre-wrap; }
<context>
<name>ModInfoRegular</name>
<message>
- <location filename="modinforegular.cpp" line="731"/>
+ <location filename="modinforegular.cpp" line="729"/>
<source>%1 contains no esp/esm/esl and no asset (textures, meshes, interface, ...) directory</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinforegular.cpp" line="735"/>
+ <location filename="modinforegular.cpp" line="733"/>
<source>Categories: &lt;br&gt;</source>
<translation type="unfinished"></translation>
</message>
@@ -3961,318 +4107,328 @@ p, li { white-space: pre-wrap; }
<context>
<name>ModList</name>
<message>
- <location filename="modlist.cpp" line="68"/>
+ <location filename="modlist.cpp" line="69"/>
<source>Game Plugins (ESP/ESM/ESL)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="69"/>
+ <location filename="modlist.cpp" line="70"/>
<source>Interface</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="70"/>
+ <location filename="modlist.cpp" line="71"/>
<source>Meshes</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="71"/>
+ <location filename="modlist.cpp" line="72"/>
<source>Bethesda Archive</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="72"/>
+ <location filename="modlist.cpp" line="73"/>
<source>Scripts (Papyrus)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="73"/>
+ <location filename="modlist.cpp" line="74"/>
<source>Script Extender Plugin</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="74"/>
+ <location filename="modlist.cpp" line="75"/>
<source>SkyProc Patcher</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="75"/>
+ <location filename="modlist.cpp" line="76"/>
<source>Sound or Music</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="76"/>
+ <location filename="modlist.cpp" line="77"/>
<source>Textures</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="77"/>
+ <location filename="modlist.cpp" line="78"/>
<source>MCM Configuration</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="78"/>
+ <location filename="modlist.cpp" line="79"/>
<source>INI files</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="79"/>
+ <location filename="modlist.cpp" line="80"/>
<source>ModGroup files</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="140"/>
+ <location filename="modlist.cpp" line="141"/>
<source>This entry contains files that have been created inside the virtual data tree (i.e. by the construction kit)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="149"/>
+ <location filename="modlist.cpp" line="150"/>
<source>Backup</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="150"/>
+ <location filename="modlist.cpp" line="151"/>
<source>Separator</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="151"/>
+ <location filename="modlist.cpp" line="152"/>
<source>No valid game data</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="152"/>
+ <location filename="modlist.cpp" line="153"/>
<source>Not endorsed yet</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="161"/>
+ <location filename="modlist.cpp" line="162"/>
+ <source>&lt;br&gt;This mod is for a different game, make sure it&apos;s compatible or it could cause crashes.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="modlist.cpp" line="164"/>
+ <source>Mod is being tracked on the website</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="modlist.cpp" line="173"/>
<source>Overwrites loose files</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="162"/>
+ <location filename="modlist.cpp" line="174"/>
<source>Overwritten loose files</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="163"/>
+ <location filename="modlist.cpp" line="175"/>
<source>Loose files Overwrites &amp; Overwritten</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="164"/>
+ <location filename="modlist.cpp" line="176"/>
<source>Redundant</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="165"/>
+ <location filename="modlist.cpp" line="177"/>
<source>Overwrites an archive with loose files</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="166"/>
+ <location filename="modlist.cpp" line="178"/>
<source>Archive is overwritten by loose files</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="167"/>
+ <location filename="modlist.cpp" line="179"/>
<source>Overwrites another archive file</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="168"/>
+ <location filename="modlist.cpp" line="180"/>
<source>Overwritten by another archive file</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="169"/>
+ <location filename="modlist.cpp" line="181"/>
<source>Archive files overwrites &amp; overwritten</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="170"/>
- <source>&lt;br&gt;This mod is for a different game, make sure it&apos;s compatible or it could cause crashes.</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="modlist.cpp" line="172"/>
- <source>Mod is being tracked on the website</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="modlist.cpp" line="264"/>
+ <location filename="modlist.cpp" line="274"/>
<source>Non-MO</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="296"/>
+ <location filename="modlist.cpp" line="306"/>
<source>invalid</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="456"/>
+ <location filename="modlist.cpp" line="475"/>
<source>installed version: &quot;%1&quot;, newest version: &quot;%2&quot;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="458"/>
+ <location filename="modlist.cpp" line="477"/>
<source>The newest version on Nexus seems to be older than the one you have installed. This could either mean the version you have has been withdrawn (i.e. due to a bug) or the author uses a non-standard versioning scheme and that newest version is actually newer. Either way you may want to &quot;upgrade&quot;.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="463"/>
+ <location filename="modlist.cpp" line="482"/>
<source>This file has been marked as &quot;Old&quot;. There is most likely an updated version of this file available.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="465"/>
+ <location filename="modlist.cpp" line="484"/>
<source>This file has been marked as &quot;Deleted&quot;! You may want to check for an update or remove the nexus ID from this mod!</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="472"/>
+ <location filename="modlist.cpp" line="491"/>
<source>%1 minute(s) and %2 second(s)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="473"/>
+ <location filename="modlist.cpp" line="492"/>
<source>This mod will be available to check in %2.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="481"/>
+ <location filename="modlist.cpp" line="500"/>
<source>Categories: &lt;br&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="512"/>
+ <location filename="modlist.cpp" line="531"/>
<source>Invalid name</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="517"/>
+ <location filename="modlist.cpp" line="536"/>
<source>Name is already in use by another mod</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="1103"/>
+ <location filename="modlist.cpp" line="1122"/>
<source>drag&amp;drop failed: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="1182"/>
+ <location filename="modlist.cpp" line="1201"/>
<source>Confirm</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="1183"/>
+ <location filename="modlist.cpp" line="1202"/>
<source>Are you sure you want to remove &quot;%1&quot;?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="1250"/>
+ <location filename="modlist.cpp" line="1295"/>
+ <source>Conflicts</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="modlist.cpp" line="1296"/>
<source>Flags</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="1251"/>
+ <location filename="modlist.cpp" line="1297"/>
<source>Content</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="1252"/>
+ <location filename="modlist.cpp" line="1298"/>
<source>Mod Name</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="1253"/>
+ <location filename="modlist.cpp" line="1299"/>
<source>Version</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="1254"/>
+ <location filename="modlist.cpp" line="1300"/>
<source>Priority</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="1255"/>
+ <location filename="modlist.cpp" line="1301"/>
<source>Category</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="1256"/>
+ <location filename="modlist.cpp" line="1302"/>
<source>Source Game</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="1257"/>
+ <location filename="modlist.cpp" line="1303"/>
<source>Nexus ID</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="1258"/>
+ <location filename="modlist.cpp" line="1304"/>
<source>Installation</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="1259"/>
+ <location filename="modlist.cpp" line="1305"/>
<source>Notes</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="1260"/>
- <location filename="modlist.cpp" line="1296"/>
+ <location filename="modlist.cpp" line="1306"/>
+ <location filename="modlist.cpp" line="1343"/>
<source>unknown</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="1268"/>
+ <location filename="modlist.cpp" line="1314"/>
<source>Name of your mods</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="1269"/>
+ <location filename="modlist.cpp" line="1315"/>
<source>Version of the mod (if available)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="1270"/>
+ <location filename="modlist.cpp" line="1316"/>
<source>Installation priority of your mod. The higher, the more &quot;important&quot; it is and thus overwrites files from mods with lower priority.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="1272"/>
+ <location filename="modlist.cpp" line="1318"/>
<source>Category of the mod.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="1273"/>
+ <location filename="modlist.cpp" line="1319"/>
<source>The source game which was the origin of this mod.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="1274"/>
+ <location filename="modlist.cpp" line="1320"/>
<source>Id of the mod as used on Nexus.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="1275"/>
- <source>Emblemes to highlight things that might require attention.</source>
+ <location filename="modlist.cpp" line="1321"/>
+ <source>Indicators of file conflicts between mods.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="modlist.cpp" line="1322"/>
+ <source>Emblems to highlight things that might require attention.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="1276"/>
+ <location filename="modlist.cpp" line="1323"/>
<source>Depicts the content of the mod:&lt;br&gt;&lt;table cellspacing=7&gt;&lt;tr&gt;&lt;td&gt;&lt;img src=&quot;:/MO/gui/content/plugin&quot; width=32/&gt;&lt;/td&gt;&lt;td&gt;Game plugins (esp/esm/esl)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;img src=&quot;:/MO/gui/content/interface&quot; width=32/&gt;&lt;/td&gt;&lt;td&gt;Interface&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;img src=&quot;:/MO/gui/content/mesh&quot; width=32/&gt;&lt;/td&gt;&lt;td&gt;Meshes&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;img src=&quot;:/MO/gui/content/bsa&quot; width=32/&gt;&lt;/td&gt;&lt;td&gt;BSA&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;img src=&quot;:/MO/gui/content/texture&quot; width=32/&gt;&lt;/td&gt;&lt;td&gt;Textures&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;img src=&quot;:/MO/gui/content/sound&quot; width=32/&gt;&lt;/td&gt;&lt;td&gt;Sounds&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;img src=&quot;:/MO/gui/content/music&quot; width=32/&gt;&lt;/td&gt;&lt;td&gt;Music&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;img src=&quot;:/MO/gui/content/string&quot; width=32/&gt;&lt;/td&gt;&lt;td&gt;Strings&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;img src=&quot;:/MO/gui/content/script&quot; width=32/&gt;&lt;/td&gt;&lt;td&gt;Scripts (Papyrus)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;img src=&quot;:/MO/gui/content/skse&quot; width=32/&gt;&lt;/td&gt;&lt;td&gt;Script Extender plugins&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;img src=&quot;:/MO/gui/content/skyproc&quot; width=32/&gt;&lt;/td&gt;&lt;td&gt;SkyProc Patcher&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;img src=&quot;:/MO/gui/content/menu&quot; width=32/&gt;&lt;/td&gt;&lt;td&gt;Mod Configuration Menu&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;img src=&quot;:/MO/gui/content/inifile&quot; width=32/&gt;&lt;/td&gt;&lt;td&gt;INI files&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;img src=&quot;:/MO/gui/content/modgroup&quot; width=32/&gt;&lt;/td&gt;&lt;td&gt;ModGroup files&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="1294"/>
+ <location filename="modlist.cpp" line="1341"/>
<source>Time this mod was installed</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modlist.cpp" line="1295"/>
+ <location filename="modlist.cpp" line="1342"/>
<source>User notes about the mod</source>
<translation type="unfinished"></translation>
</message>
@@ -4280,7 +4436,7 @@ p, li { white-space: pre-wrap; }
<context>
<name>ModListSortProxy</name>
<message>
- <location filename="modlistsortproxy.cpp" line="506"/>
+ <location filename="modlistsortproxy.cpp" line="622"/>
<source>Drag&amp;Drop is only supported when sorting by priority</source>
<translation type="unfinished"></translation>
</message>
@@ -4301,51 +4457,17 @@ p, li { white-space: pre-wrap; }
<context>
<name>MyFileSystemModel</name>
<message>
- <location filename="overwriteinfodialog.cpp" line="49"/>
+ <location filename="overwriteinfodialog.cpp" line="48"/>
<source>Overwrites</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="overwriteinfodialog.cpp" line="61"/>
+ <location filename="overwriteinfodialog.cpp" line="60"/>
<source>not implemented</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
- <name>NXMAccessManager</name>
- <message>
- <location filename="nxmaccessmanager.cpp" line="128"/>
- <location filename="nxmaccessmanager.cpp" line="146"/>
- <source>Validating Nexus Connection</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="nxmaccessmanager.cpp" line="246"/>
- <source>There was a timeout during the request</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="nxmaccessmanager.cpp" line="268"/>
- <source>Unknown error</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="nxmaccessmanager.cpp" line="308"/>
- <source>Validation failed, please reauthenticate in the Settings -&gt; Nexus tab: %1</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="nxmaccessmanager.cpp" line="313"/>
- <source>Could not parse response. Invalid JSON.</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="nxmaccessmanager.cpp" line="319"/>
- <source>Unknown error.</source>
- <translation type="unfinished"></translation>
- </message>
-</context>
-<context>
<name>NXMUrl</name>
<message>
<location filename="../../uibase/src/nxmurl.cpp" line="34"/>
@@ -4356,32 +4478,32 @@ p, li { white-space: pre-wrap; }
<context>
<name>NexusInterface</name>
<message>
- <location filename="nexusinterface.cpp" line="284"/>
- <source>Failed to guess mod id for &quot;%1&quot;, please pick the correct one</source>
+ <location filename="nexusinterface.cpp" line="299"/>
+ <source>Please pick the mod ID for &quot;%1&quot;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="nexusinterface.cpp" line="674"/>
+ <location filename="nexusinterface.cpp" line="697"/>
<source>You must authorize MO2 in Settings -&gt; Nexus to use the Nexus API.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="nexusinterface.cpp" line="683"/>
+ <location filename="nexusinterface.cpp" line="706"/>
<source>You&apos;ve exceeded the Nexus API rate limit and requests are now being throttled. Your next batch of requests will be available in approximately %1 minutes and %2 seconds.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="nexusinterface.cpp" line="735"/>
+ <location filename="nexusinterface.cpp" line="762"/>
<source>Aborting download: Either you clicked on a premium-only link and your account is not premium, or the download link was generated by a different account than the one stored in Mod Organizer.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="nexusinterface.cpp" line="842"/>
+ <location filename="nexusinterface.cpp" line="869"/>
<source>empty response</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="nexusinterface.cpp" line="900"/>
+ <location filename="nexusinterface.cpp" line="927"/>
<source>invalid response</source>
<translation type="unfinished"></translation>
</message>
@@ -4432,27 +4554,27 @@ p, li { white-space: pre-wrap; }
<context>
<name>NexusTab</name>
<message>
- <location filename="modinfodialognexus.cpp" line="130"/>
+ <location filename="modinfodialognexus.cpp" line="131"/>
<source>Current Version: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialognexus.cpp" line="134"/>
+ <location filename="modinfodialognexus.cpp" line="135"/>
<source>No update available</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialognexus.cpp" line="166"/>
+ <location filename="modinfodialognexus.cpp" line="167"/>
<source>Tracked</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialognexus.cpp" line="169"/>
+ <location filename="modinfodialognexus.cpp" line="170"/>
<source>Untracked</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialognexus.cpp" line="226"/>
+ <location filename="modinfodialognexus.cpp" line="283"/>
<source>
&lt;div style=&quot;text-align: center;&quot;&gt;
&lt;p&gt;This mod does not have a valid Nexus ID. You can add a custom web
@@ -4464,7 +4586,7 @@ p, li { white-space: pre-wrap; }
<context>
<name>NoConflictListModel</name>
<message>
- <location filename="modinfodialogconflicts.cpp" line="322"/>
+ <location filename="modinfodialogconflicts.cpp" line="325"/>
<source>File</source>
<translation type="unfinished"></translation>
</message>
@@ -4472,269 +4594,211 @@ p, li { white-space: pre-wrap; }
<context>
<name>OrganizerCore</name>
<message>
- <location filename="organizercore.cpp" line="393"/>
- <location filename="organizercore.cpp" line="420"/>
- <source>Failed to write settings</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="organizercore.cpp" line="394"/>
- <source>An error occurred trying to update MO settings to %1: %2</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="organizercore.cpp" line="415"/>
+ <location filename="organizercore.cpp" line="181"/>
<source>File is write protected</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="417"/>
+ <location filename="organizercore.cpp" line="183"/>
<source>Invalid file format (probably a bug)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="418"/>
+ <location filename="organizercore.cpp" line="185"/>
<source>Unknown error %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="421"/>
+ <location filename="organizercore.cpp" line="189"/>
+ <source>Failed to write settings</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="organizercore.cpp" line="190"/>
<source>An error occurred trying to write back MO settings to %1: %2</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="646"/>
- <location filename="organizercore.cpp" line="657"/>
+ <location filename="organizercore.cpp" line="366"/>
+ <location filename="organizercore.cpp" line="377"/>
<source>Download started</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="660"/>
+ <location filename="organizercore.cpp" line="380"/>
<source>Download failed</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="958"/>
- <location filename="organizercore.cpp" line="995"/>
- <location filename="organizercore.cpp" line="1006"/>
- <location filename="organizercore.cpp" line="1064"/>
+ <location filename="organizercore.cpp" line="701"/>
+ <location filename="organizercore.cpp" line="752"/>
<source>Installation cancelled</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="959"/>
- <location filename="organizercore.cpp" line="1007"/>
+ <location filename="organizercore.cpp" line="702"/>
+ <location filename="organizercore.cpp" line="753"/>
<source>Another installation is currently in progress.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="971"/>
- <location filename="organizercore.cpp" line="1036"/>
+ <location filename="organizercore.cpp" line="714"/>
+ <location filename="organizercore.cpp" line="782"/>
<source>Installation successful</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="979"/>
- <location filename="organizercore.cpp" line="1046"/>
+ <location filename="organizercore.cpp" line="722"/>
+ <location filename="organizercore.cpp" line="792"/>
<source>Configure Mod</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="980"/>
- <location filename="organizercore.cpp" line="1047"/>
+ <location filename="organizercore.cpp" line="723"/>
+ <location filename="organizercore.cpp" line="793"/>
<source>This mod contains ini tweaks. Do you want to configure them now?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="992"/>
- <location filename="organizercore.cpp" line="1057"/>
+ <location filename="organizercore.cpp" line="735"/>
+ <location filename="organizercore.cpp" line="803"/>
<source>mod not found: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="996"/>
- <location filename="organizercore.cpp" line="1065"/>
- <source>The mod was not installed completely.</source>
+ <location filename="organizercore.cpp" line="738"/>
+ <location filename="organizercore.cpp" line="809"/>
+ <source>Extraction cancelled</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="1335"/>
+ <location filename="organizercore.cpp" line="739"/>
+ <location filename="organizercore.cpp" line="810"/>
+ <source>The installation was cancelled while extracting files. If this was prior to a FOMOD setup, this warning may be ignored. However, if this was during installation, the mod will likely be missing files.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="organizercore.cpp" line="982"/>
<source>file not found: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="1348"/>
+ <location filename="organizercore.cpp" line="996"/>
<source>failed to generate preview for %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="1409"/>
+ <location filename="organizercore.cpp" line="1048"/>
<source>Sorry</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="1410"/>
+ <location filename="organizercore.cpp" line="1049"/>
<source>Sorry, can&apos;t preview anything. This function currently does not support extracting from bsas.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="1420"/>
+ <location filename="organizercore.cpp" line="1059"/>
<source>File &apos;%1&apos; not found.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="1428"/>
+ <location filename="organizercore.cpp" line="1067"/>
<source>Failed to generate preview for %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="1525"/>
- <source>Executable not found: %1</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="organizercore.cpp" line="1560"/>
- <source>Start Steam?</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="organizercore.cpp" line="1561"/>
- <source>Steam is required to be running already to correctly start the game. Should MO try to start steam now?</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="organizercore.cpp" line="1584"/>
- <source>Steam: Access Denied</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="organizercore.cpp" line="1585"/>
- <source>MO was denied access to the Steam process. This normally indicates that Steam is being run as administrator while MO is not. This can cause issues launching the game. It is recommended to not run Steam as administrator unless absolutely necessary.
-
-Restart MO as administrator?</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="organizercore.cpp" line="1632"/>
- <source>Error</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="organizercore.cpp" line="1640"/>
- <source>Windows Event Log Error</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="organizercore.cpp" line="1641"/>
- <source>The Windows Event Log service is disabled and/or not running. This prevents USVFS from running properly. Your mods may not be working in the executable that you are launching. Note that you may have to restart MO and/or your PC after the service is fixed.
-
-Continue launching %1?</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="organizercore.cpp" line="1653"/>
- <source>Blacklisted Executable</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="organizercore.cpp" line="1654"/>
- <source>The executable you are attempted to launch is blacklisted in the virtual file system. This will likely prevent the executable, and any executables that are launched by this one, from seeing any mods. This could extend to INI files, save games and any other virtualized files.
-
-Continue launching %1?</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="organizercore.cpp" line="1752"/>
- <source>No profile set</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="organizercore.cpp" line="2059"/>
+ <location filename="organizercore.cpp" line="1133"/>
<source>Failed to refresh list of esps: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="2168"/>
+ <location filename="organizercore.cpp" line="1242"/>
<source>Multiple esps/esls activated, please check that they don&apos;t conflict.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="2232"/>
+ <location filename="organizercore.cpp" line="1306"/>
<source>You need to be logged in with Nexus</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="2271"/>
+ <location filename="organizercore.cpp" line="1345"/>
<source>Download?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="2272"/>
+ <location filename="organizercore.cpp" line="1346"/>
<source>A download has been started but no installed page plugin recognizes it.
If you download anyway no information (i.e. version) will be associated with the download.
Continue?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="2407"/>
- <location filename="organizercore.cpp" line="2456"/>
+ <location filename="organizercore.cpp" line="1483"/>
+ <location filename="organizercore.cpp" line="1532"/>
<source>failed to update mod list: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="2463"/>
- <location filename="organizercore.cpp" line="2480"/>
+ <location filename="organizercore.cpp" line="1539"/>
+ <location filename="organizercore.cpp" line="1556"/>
<source>login successful</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="2487"/>
+ <location filename="organizercore.cpp" line="1566"/>
<source>Login failed</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="2488"/>
+ <location filename="organizercore.cpp" line="1567"/>
<source>Login failed, try again?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="2497"/>
+ <location filename="organizercore.cpp" line="1576"/>
<source>login failed: %1. Download will not be associated with an account</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="2505"/>
+ <location filename="organizercore.cpp" line="1584"/>
<source>login failed: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="2515"/>
+ <location filename="organizercore.cpp" line="1594"/>
<source>login failed: %1. You need to log-in with Nexus to update MO.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="2568"/>
+ <location filename="organizercore.cpp" line="1647"/>
<source>MO1 &quot;Script Extender&quot; load mechanism has left hook.dll in your game folder</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="2571"/>
- <location filename="organizercore.cpp" line="2587"/>
+ <location filename="organizercore.cpp" line="1650"/>
+ <location filename="organizercore.cpp" line="1666"/>
<source>Description missing</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="2580"/>
+ <location filename="organizercore.cpp" line="1659"/>
<source>&lt;a href=&quot;%1&quot;&gt;hook.dll&lt;/a&gt; has been found in your game folder (right click to copy the full path). This is most likely a leftover of setting the ModOrganizer 1 load mechanism to &quot;Script Extender&quot;, in which case you must remove this file either by changing the load mechanism in ModOrganizer 1 or manually removing the file, otherwise the game is likely to crash and burn.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="2614"/>
+ <location filename="organizercore.cpp" line="1693"/>
<source>failed to save load order: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="2686"/>
+ <location filename="organizercore.cpp" line="1771"/>
+ <source>Error</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="organizercore.cpp" line="1845"/>
<source>The designated write target &quot;%1&quot; is not enabled.</source>
<translation type="unfinished"></translation>
</message>
@@ -4742,12 +4806,12 @@ Continue?</source>
<context>
<name>OverwriteConflictListModel</name>
<message>
- <location filename="modinfodialogconflicts.cpp" line="296"/>
+ <location filename="modinfodialogconflicts.cpp" line="299"/>
<source>File</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialogconflicts.cpp" line="297"/>
+ <location filename="modinfodialogconflicts.cpp" line="300"/>
<source>Overwritten Mods</source>
<translation type="unfinished"></translation>
</message>
@@ -4770,63 +4834,63 @@ Continue?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="overwriteinfodialog.cpp" line="90"/>
+ <location filename="overwriteinfodialog.cpp" line="89"/>
<source>&amp;Delete</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="overwriteinfodialog.cpp" line="91"/>
+ <location filename="overwriteinfodialog.cpp" line="90"/>
<source>&amp;Rename</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="overwriteinfodialog.cpp" line="92"/>
+ <location filename="overwriteinfodialog.cpp" line="91"/>
<source>&amp;Open</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="overwriteinfodialog.cpp" line="93"/>
+ <location filename="overwriteinfodialog.cpp" line="92"/>
<source>&amp;New Folder</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="overwriteinfodialog.cpp" line="114"/>
+ <location filename="overwriteinfodialog.cpp" line="136"/>
<source>mod not found: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="overwriteinfodialog.cpp" line="149"/>
+ <location filename="overwriteinfodialog.cpp" line="171"/>
<source>Failed to delete &quot;%1&quot;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="overwriteinfodialog.cpp" line="165"/>
- <location filename="overwriteinfodialog.cpp" line="171"/>
- <location filename="overwriteinfodialog.cpp" line="190"/>
- <location filename="overwriteinfodialog.cpp" line="195"/>
+ <location filename="overwriteinfodialog.cpp" line="187"/>
+ <location filename="overwriteinfodialog.cpp" line="193"/>
+ <location filename="overwriteinfodialog.cpp" line="212"/>
+ <location filename="overwriteinfodialog.cpp" line="217"/>
<source>Confirm</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="overwriteinfodialog.cpp" line="165"/>
- <location filename="overwriteinfodialog.cpp" line="190"/>
+ <location filename="overwriteinfodialog.cpp" line="187"/>
+ <location filename="overwriteinfodialog.cpp" line="212"/>
<source>Are you sure you want to delete &quot;%1&quot;?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="overwriteinfodialog.cpp" line="171"/>
- <location filename="overwriteinfodialog.cpp" line="195"/>
+ <location filename="overwriteinfodialog.cpp" line="193"/>
+ <location filename="overwriteinfodialog.cpp" line="217"/>
<source>Are you sure you want to delete the selected files?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="overwriteinfodialog.cpp" line="240"/>
- <location filename="overwriteinfodialog.cpp" line="246"/>
+ <location filename="overwriteinfodialog.cpp" line="262"/>
+ <location filename="overwriteinfodialog.cpp" line="268"/>
<source>New Folder</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="overwriteinfodialog.cpp" line="252"/>
+ <location filename="overwriteinfodialog.cpp" line="274"/>
<source>Failed to create &quot;%1&quot;</source>
<translation type="unfinished"></translation>
</message>
@@ -4834,12 +4898,12 @@ Continue?</source>
<context>
<name>OverwrittenConflictListModel</name>
<message>
- <location filename="modinfodialogconflicts.cpp" line="309"/>
+ <location filename="modinfodialogconflicts.cpp" line="312"/>
<source>File</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialogconflicts.cpp" line="310"/>
+ <location filename="modinfodialogconflicts.cpp" line="313"/>
<source>Providing Mod</source>
<translation type="unfinished"></translation>
</message>
@@ -4847,18 +4911,18 @@ Continue?</source>
<context>
<name>PluginContainer</name>
<message>
- <location filename="plugincontainer.cpp" line="330"/>
+ <location filename="plugincontainer.cpp" line="333"/>
<source>Some plugins could not be loaded</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="plugincontainer.cpp" line="333"/>
- <location filename="plugincontainer.cpp" line="351"/>
+ <location filename="plugincontainer.cpp" line="336"/>
+ <location filename="plugincontainer.cpp" line="354"/>
<source>Description missing</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="plugincontainer.cpp" line="342"/>
+ <location filename="plugincontainer.cpp" line="345"/>
<source>The following plugins could not be loaded. The reason may be missing dependencies (i.e. python) or an outdated version:</source>
<translation type="unfinished"></translation>
</message>
@@ -4866,135 +4930,159 @@ Continue?</source>
<context>
<name>PluginList</name>
<message>
- <location filename="pluginlist.cpp" line="104"/>
+ <location filename="pluginlist.cpp" line="111"/>
<source>Name</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="pluginlist.cpp" line="105"/>
+ <location filename="pluginlist.cpp" line="112"/>
<source>Priority</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="pluginlist.cpp" line="106"/>
+ <location filename="pluginlist.cpp" line="113"/>
<source>Mod Index</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="pluginlist.cpp" line="107"/>
+ <location filename="pluginlist.cpp" line="114"/>
<source>Flags</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="pluginlist.cpp" line="108"/>
- <location filename="pluginlist.cpp" line="120"/>
+ <location filename="pluginlist.cpp" line="115"/>
+ <location filename="pluginlist.cpp" line="128"/>
<source>unknown</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="pluginlist.cpp" line="116"/>
- <source>Name of your mods</source>
+ <location filename="pluginlist.cpp" line="123"/>
+ <source>Name of the plugin</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="pluginlist.cpp" line="117"/>
- <source>Load priority of your mod. The higher, the more &quot;important&quot; it is and thus overwrites data from plugins with lower priority.</source>
+ <location filename="pluginlist.cpp" line="124"/>
+ <source>Emblems to highlight things that might require attention.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="pluginlist.cpp" line="119"/>
- <source>The modindex determines the formids of objects originating from this mods.</source>
+ <location filename="pluginlist.cpp" line="125"/>
+ <source>Load priority of plugins. The higher, the more &quot;important&quot; it is and thus overwrites data from plugins with lower priority.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="pluginlist.cpp" line="228"/>
+ <location filename="pluginlist.cpp" line="127"/>
+ <source>Determines the formids of objects originating from this mods.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="pluginlist.cpp" line="239"/>
<source>failed to update esp info for file %1 (source id: %2), error: %3</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="pluginlist.cpp" line="301"/>
+ <location filename="pluginlist.cpp" line="312"/>
<source>Plugin not found: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="pluginlist.cpp" line="349"/>
- <location filename="pluginlist.cpp" line="361"/>
+ <location filename="pluginlist.cpp" line="360"/>
+ <location filename="pluginlist.cpp" line="372"/>
<source>Confirm</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="pluginlist.cpp" line="349"/>
+ <location filename="pluginlist.cpp" line="360"/>
<source>Really enable all plugins?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="pluginlist.cpp" line="361"/>
+ <location filename="pluginlist.cpp" line="372"/>
<source>Really disable all plugins?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="pluginlist.cpp" line="466"/>
+ <location filename="pluginlist.cpp" line="492"/>
<source>The file containing locked plugin indices is broken</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="pluginlist.cpp" line="963"/>
- <location filename="pluginlist.cpp" line="967"/>
- <source>&lt;b&gt;Origin&lt;/b&gt;: %1</source>
+ <location filename="pluginlist.cpp" line="1037"/>
+ <source>Origin</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="pluginlist.cpp" line="964"/>
- <source>&lt;br&gt;&lt;b&gt;&lt;i&gt;This plugin can&apos;t be disabled (enforced by the game).&lt;/i&gt;&lt;/b&gt;</source>
+ <location filename="pluginlist.cpp" line="1042"/>
+ <source>This plugin can&apos;t be disabled (enforced by the game).</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="pluginlist.cpp" line="969"/>
+ <location filename="pluginlist.cpp" line="1047"/>
<source>Author</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="pluginlist.cpp" line="972"/>
+ <location filename="pluginlist.cpp" line="1053"/>
<source>Description</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="pluginlist.cpp" line="975"/>
+ <location filename="pluginlist.cpp" line="1059"/>
<source>Missing Masters</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="pluginlist.cpp" line="982"/>
+ <location filename="pluginlist.cpp" line="1070"/>
<source>Enabled Masters</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="pluginlist.cpp" line="985"/>
+ <location filename="pluginlist.cpp" line="1076"/>
<source>Loads Archives</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="pluginlist.cpp" line="986"/>
+ <location filename="pluginlist.cpp" line="1078"/>
<source>There are Archives connected to this plugin. Their assets will be added to your game, overwriting in case of conflicts following the plugin order. Loose files will always overwrite assets from Archives. (This flag only checks for Archives from the same mod as the plugin)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="pluginlist.cpp" line="991"/>
+ <location filename="pluginlist.cpp" line="1088"/>
<source>Loads INI settings</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="pluginlist.cpp" line="992"/>
+ <location filename="pluginlist.cpp" line="1089"/>
<source>There is an ini file connected to this plugin. Its settings will be added to your game settings, overwriting in case of conflicts.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="pluginlist.cpp" line="996"/>
+ <location filename="pluginlist.cpp" line="1096"/>
<source>This ESP is flagged as an ESL. It will adhere to the ESP load order but the records will be loaded in ESL space.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="pluginlist.cpp" line="1171"/>
+ <location filename="pluginlist.cpp" line="1130"/>
+ <source>Incompatible with %1</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="pluginlist.cpp" line="1136"/>
+ <source>Depends on missing %1</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="pluginlist.cpp" line="1145"/>
+ <source>Warning</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="pluginlist.cpp" line="1149"/>
+ <source>Error</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="pluginlist.cpp" line="1401"/>
<source>failed to restore load order for %1</source>
<translation type="unfinished"></translation>
</message>
@@ -5033,7 +5121,7 @@ Continue?</source>
&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;
p, li { white-space: pre-wrap; }
&lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:&apos;MS Shell Dlg 2&apos;; font-size:8.25pt; font-weight:400; font-style:normal;&quot;&gt;
-&lt;p style=&quot;-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-size:7.8pt;&quot;&gt;&lt;br /&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;Click a notification above to get more details...&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -5042,18 +5130,18 @@ p, li { white-space: pre-wrap; }
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="problemsdialog.cpp" line="56"/>
- <location filename="problemsdialog.cpp" line="57"/>
+ <location filename="problemsdialog.cpp" line="62"/>
+ <location filename="problemsdialog.cpp" line="63"/>
<source>Fix</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="problemsdialog.cpp" line="63"/>
+ <location filename="problemsdialog.cpp" line="69"/>
<source>No guided fix</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="problemsdialog.cpp" line="71"/>
+ <location filename="problemsdialog.cpp" line="77"/>
<source>(There are no notifications)</source>
<translation type="unfinished"></translation>
</message>
@@ -5061,17 +5149,17 @@ p, li { white-space: pre-wrap; }
<context>
<name>Profile</name>
<message>
- <location filename="profile.cpp" line="81"/>
+ <location filename="profile.cpp" line="80"/>
<source>invalid profile name: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="profile.cpp" line="85"/>
+ <location filename="profile.cpp" line="84"/>
<source>failed to create %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="profile.cpp" line="259"/>
+ <location filename="profile.cpp" line="257"/>
<source>failed to write mod list: %1</source>
<translation type="unfinished"></translation>
</message>
@@ -5081,53 +5169,51 @@ p, li { white-space: pre-wrap; }
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="profile.cpp" line="290"/>
+ <location filename="profile.cpp" line="292"/>
<source>failed to create tweaked ini: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="profile.cpp" line="315"/>
+ <location filename="profile.cpp" line="319"/>
<source>failed to open %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="profile.cpp" line="374"/>
+ <location filename="profile.cpp" line="379"/>
<source>&quot;%1&quot; is missing or inaccessible</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="profile.cpp" line="419"/>
- <location filename="profile.cpp" line="458"/>
- <location filename="profile.cpp" line="548"/>
- <location filename="profile.cpp" line="569"/>
- <location filename="profile.cpp" line="579"/>
- <location filename="profile.cpp" line="598"/>
- <location filename="profile.cpp" line="608"/>
+ <location filename="profile.cpp" line="424"/>
+ <location filename="profile.cpp" line="465"/>
+ <location filename="profile.cpp" line="557"/>
+ <location filename="profile.cpp" line="607"/>
+ <location filename="profile.cpp" line="617"/>
<source>invalid mod index: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="profile.cpp" line="540"/>
+ <location filename="profile.cpp" line="549"/>
<source>invalid priority %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="profile.cpp" line="779"/>
+ <location filename="profile.cpp" line="788"/>
<source>Delete profile-specific save games?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="profile.cpp" line="780"/>
+ <location filename="profile.cpp" line="789"/>
<source>Do you want to delete the profile-specific save games? (If you select &quot;No&quot;, the save games will show up again if you re-enable profile-specific save games)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="profile.cpp" line="810"/>
+ <location filename="profile.cpp" line="819"/>
<source>Missing profile-specific game INI files!</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="profile.cpp" line="811"/>
+ <location filename="profile.cpp" line="820"/>
<source>Some of your profile-specific game INI files were missing. They will now be copied from the vanilla game folder. You might want to double-check your settings.
Missing files:
@@ -5135,12 +5221,12 @@ Missing files:
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="profile.cpp" line="827"/>
+ <location filename="profile.cpp" line="836"/>
<source>Delete profile-specific game INI files?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="profile.cpp" line="828"/>
+ <location filename="profile.cpp" line="837"/>
<source>Do you want to delete the profile-specific game INI files? (If you select &quot;No&quot;, the INI files will be used again if you re-enable profile-specific game INI files.)</source>
<translation type="unfinished"></translation>
</message>
@@ -5310,168 +5396,347 @@ p, li { white-space: pre-wrap; }
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="profilesdialog.cpp" line="115"/>
- <location filename="profilesdialog.cpp" line="156"/>
+ <location filename="profilesdialog.cpp" line="121"/>
+ <location filename="profilesdialog.cpp" line="162"/>
<source>failed to create profile: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="profilesdialog.cpp" line="165"/>
+ <location filename="profilesdialog.cpp" line="171"/>
<source>Name</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="profilesdialog.cpp" line="165"/>
+ <location filename="profilesdialog.cpp" line="171"/>
<source>Please enter a name for the new profile</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="profilesdialog.cpp" line="176"/>
+ <location filename="profilesdialog.cpp" line="182"/>
<source>failed to copy profile: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="profilesdialog.cpp" line="179"/>
+ <location filename="profilesdialog.cpp" line="185"/>
<source>Invalid name</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="profilesdialog.cpp" line="180"/>
+ <location filename="profilesdialog.cpp" line="186"/>
<source>Invalid profile name</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="profilesdialog.cpp" line="189"/>
+ <location filename="profilesdialog.cpp" line="195"/>
<source>Deleting active profile</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="profilesdialog.cpp" line="190"/>
+ <location filename="profilesdialog.cpp" line="196"/>
<source>Unable to delete active profile. Please change to a different profile first.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="profilesdialog.cpp" line="194"/>
+ <location filename="profilesdialog.cpp" line="200"/>
<source>Confirm</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="profilesdialog.cpp" line="194"/>
+ <location filename="profilesdialog.cpp" line="200"/>
<source>Are you sure you want to remove this profile (including profile-specific save games, if any)?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="profilesdialog.cpp" line="202"/>
+ <location filename="profilesdialog.cpp" line="208"/>
<source>Profile broken</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="profilesdialog.cpp" line="203"/>
+ <location filename="profilesdialog.cpp" line="209"/>
<source>This profile you&apos;re about to delete seems to be broken or the path is invalid. I&apos;m about to delete the following folder: &quot;%1&quot;. Proceed?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="profilesdialog.cpp" line="235"/>
+ <location filename="profilesdialog.cpp" line="241"/>
<source>Rename Profile</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="profilesdialog.cpp" line="235"/>
+ <location filename="profilesdialog.cpp" line="241"/>
<source>New Name</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="profilesdialog.cpp" line="270"/>
+ <location filename="profilesdialog.cpp" line="276"/>
<source>failed to change archive invalidation state: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="profilesdialog.cpp" line="304"/>
+ <location filename="profilesdialog.cpp" line="310"/>
<source>failed to determine if invalidation is active: %1</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
- <name>QApplication</name>
+ <name>QObject</name>
<message>
- <location filename="../../uibase/src/registry.cpp" line="36"/>
+ <location filename="../../uibase/src/registry.cpp" line="42"/>
+ <location filename="../../uibase/src/registry.cpp" line="43"/>
+ <location filename="../../uibase/src/textviewer.cpp" line="137"/>
+ <location filename="../../uibase/src/textviewer.cpp" line="138"/>
<source>INI file is read-only</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../../uibase/src/registry.cpp" line="37"/>
- <location filename="../../uibase/src/textviewer.cpp" line="133"/>
- <source>Mod Organizer is attempting to write to &quot;%1&quot; which is currently set to read-only. Clear the read-only flag to allow the write?</source>
+ <location filename="../../uibase/src/registry.cpp" line="44"/>
+ <location filename="../../uibase/src/textviewer.cpp" line="139"/>
+ <source>Mod Organizer is attempting to write to &quot;%1&quot; which is currently set to read-only.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../../uibase/src/textviewer.cpp" line="132"/>
- <source>File is read-only</source>
+ <location filename="../../uibase/src/registry.cpp" line="47"/>
+ <location filename="../../uibase/src/textviewer.cpp" line="142"/>
+ <source>Clear the read-only flag</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../../uibase/src/registry.cpp" line="50"/>
+ <location filename="../../uibase/src/textviewer.cpp" line="145"/>
+ <source>Allow the write once</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../../uibase/src/registry.cpp" line="51"/>
+ <location filename="../../uibase/src/textviewer.cpp" line="146"/>
+ <source>The file will be set to read-only again.</source>
<translation type="unfinished"></translation>
</message>
-</context>
-<context>
- <name>QObject</name>
<message>
- <location filename="../../uibase/src/report.cpp" line="34"/>
- <location filename="../../uibase/src/report.cpp" line="37"/>
- <location filename="main.cpp" line="98"/>
- <location filename="organizercore.cpp" line="684"/>
- <location filename="organizercore.cpp" line="699"/>
+ <location filename="../../uibase/src/registry.cpp" line="54"/>
+ <location filename="../../uibase/src/textviewer.cpp" line="149"/>
+ <source>Skip this file</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../../uibase/src/report.cpp" line="38"/>
+ <location filename="../../uibase/src/report.cpp" line="41"/>
+ <location filename="loot.cpp" line="413"/>
+ <location filename="main.cpp" line="104"/>
+ <location filename="organizercore.cpp" line="404"/>
+ <location filename="settingsdialogdiagnostics.cpp" line="37"/>
+ <location filename="settingsdialogdiagnostics.cpp" line="61"/>
+ <location filename="settingsdialogpaths.cpp" line="71"/>
<source>Error</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../../uibase/src/safewritefile.cpp" line="33"/>
- <source>failed to open temporary file</source>
+ <location filename="../../uibase/src/report.cpp" line="289"/>
+ <source>You can reset these choices by clicking &quot;Reset Dialog Choices&quot; in the General tab of the Settings</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../../uibase/src/utility.cpp" line="88"/>
+ <location filename="../../uibase/src/report.cpp" line="298"/>
+ <source>Always ask</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../../uibase/src/report.cpp" line="299"/>
+ <location filename="../../uibase/src/report.cpp" line="306"/>
+ <source>Remember my choice</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../../uibase/src/report.cpp" line="300"/>
+ <source>Remember my choice for %1</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../../uibase/src/safewritefile.cpp" line="43"/>
+ <source>Failed to save &apos;{}&apos;, could not create a temporary file: {} (error {})</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../../uibase/src/utility.cpp" line="67"/>
<source>removal of &quot;%1&quot; failed: %2</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../../uibase/src/utility.cpp" line="95"/>
+ <location filename="../../uibase/src/utility.cpp" line="74"/>
<source>removal of &quot;%1&quot; failed</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../../uibase/src/utility.cpp" line="99"/>
+ <location filename="../../uibase/src/utility.cpp" line="78"/>
<source>&quot;%1&quot; doesn&apos;t exist (remove)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../../uibase/src/utility.cpp" line="444"/>
- <location filename="../../uibase/src/utility.cpp" line="469"/>
+ <location filename="../../uibase/src/utility.cpp" line="311"/>
+ <source>Error %1</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../../uibase/src/utility.cpp" line="527"/>
+ <location filename="../../uibase/src/utility.cpp" line="552"/>
<source>failed to create directory &quot;%1&quot;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../../uibase/src/utility.cpp" line="453"/>
- <location filename="../../uibase/src/utility.cpp" line="476"/>
+ <location filename="../../uibase/src/utility.cpp" line="536"/>
+ <location filename="../../uibase/src/utility.cpp" line="559"/>
<source>failed to copy &quot;%1&quot; to &quot;%2&quot;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="categories.cpp" line="149"/>
+ <location filename="../../uibase/src/utility.cpp" line="807"/>
+ <source>%1 MB</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../../uibase/src/utility.cpp" line="807"/>
+ <source>%1 GB</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../../uibase/src/utility.cpp" line="807"/>
+ <source>%1 TB</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../../uibase/src/utility.cpp" line="810"/>
+ <source>%1 KB</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../../uibase/src/utility.cpp" line="828"/>
+ <source>%1 B/s</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../../uibase/src/utility.cpp" line="832"/>
+ <source>%1 KB/s</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../../uibase/src/utility.cpp" line="836"/>
+ <source>%1 MB/s</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="apiuseraccount.cpp" line="8"/>
+ <source>Regular</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="apiuseraccount.cpp" line="11"/>
+ <source>Premium</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="apiuseraccount.cpp" line="15"/>
+ <location filename="settingsdialogdiagnostics.cpp" line="81"/>
+ <source>None</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="categories.cpp" line="150"/>
<source>Failed to save custom categories</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="categories.cpp" line="271"/>
- <location filename="categories.cpp" line="306"/>
- <location filename="categories.cpp" line="316"/>
- <location filename="categories.cpp" line="326"/>
+ <location filename="categories.cpp" line="272"/>
+ <location filename="categories.cpp" line="307"/>
+ <location filename="categories.cpp" line="317"/>
+ <location filename="categories.cpp" line="362"/>
<source>invalid category index: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
+ <location filename="categories.cpp" line="328"/>
+ <source>Active</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="categories.cpp" line="329"/>
+ <source>Update available</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="categories.cpp" line="330"/>
+ <source>Has category</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="categories.cpp" line="331"/>
+ <source>Conflicted</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="categories.cpp" line="332"/>
+ <source>Endorsed</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="categories.cpp" line="333"/>
+ <source>Has backup</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="categories.cpp" line="334"/>
+ <source>Managed</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="categories.cpp" line="335"/>
+ <source>Has valid game data</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="categories.cpp" line="336"/>
+ <source>Has Nexus ID</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
<location filename="categories.cpp" line="337"/>
+ <source>Tracked on Nexus</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="categories.cpp" line="373"/>
<source>invalid category id: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
+ <location filename="colortable.cpp" line="241"/>
+ <source>Is overwritten (loose files)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="colortable.cpp" line="247"/>
+ <source>Is overwriting (loose files)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="colortable.cpp" line="253"/>
+ <source>Is overwritten (archives)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="colortable.cpp" line="259"/>
+ <source>Is overwriting (archives)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="colortable.cpp" line="265"/>
+ <source>Mod contains selected plugin</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="colortable.cpp" line="271"/>
+ <source>Plugin is contained in selected mod</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
<location filename="csvbuilder.cpp" line="70"/>
<source>invalid field name &quot;%1&quot;</source>
<translation type="unfinished"></translation>
@@ -5512,53 +5777,48 @@ p, li { white-space: pre-wrap; }
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="filerenamer.cpp" line="100"/>
+ <location filename="filerenamer.cpp" line="111"/>
<source>The hidden file &quot;%1&quot; already exists. Replace it?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="filerenamer.cpp" line="103"/>
+ <location filename="filerenamer.cpp" line="114"/>
<source>The visible file &quot;%1&quot; already exists. Replace it?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="filerenamer.cpp" line="113"/>
+ <location filename="filerenamer.cpp" line="124"/>
<source>Replace file?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="filerenamer.cpp" line="152"/>
- <location filename="filerenamer.cpp" line="176"/>
+ <location filename="filerenamer.cpp" line="164"/>
+ <location filename="filerenamer.cpp" line="190"/>
<source>File operation failed</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="filerenamer.cpp" line="153"/>
- <source>Failed to remove &quot;%1&quot;. Maybe you lack the required file permissions?</source>
+ <location filename="filerenamer.cpp" line="165"/>
+ <source>Failed to remove &quot;%1&quot;: %2</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="filerenamer.cpp" line="177"/>
- <source>failed to rename %1 to %2</source>
+ <location filename="filerenamer.cpp" line="191"/>
+ <source>Failed to rename file: %1.<byte value="xd"/>
+<byte value="xd"/>
+Source:<byte value="xd"/>
+&quot;%2&quot;<byte value="xd"/>
+<byte value="xd"/>
+Destination:<byte value="xd"/>
+&quot;%3&quot;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="filterwidget.cpp" line="48"/>
+ <location filename="filterwidget.cpp" line="51"/>
<source>Filter</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="helper.cpp" line="56"/>
- <location filename="helper.cpp" line="65"/>
- <source>helper failed</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="helper.cpp" line="81"/>
- <source>failed to determine account name</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
<location filename="installationmanager.cpp" line="73"/>
<location filename="selfupdater.cpp" line="82"/>
<source>invalid 7-zip32.dll: %1</source>
@@ -5570,326 +5830,354 @@ p, li { white-space: pre-wrap; }
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="instancemanager.cpp" line="82"/>
+ <location filename="instancemanager.cpp" line="83"/>
<source>Deleting folder</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="instancemanager.cpp" line="83"/>
+ <location filename="instancemanager.cpp" line="84"/>
<source>I&apos;m about to delete the following folder: &quot;%1&quot;. Proceed?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="instancemanager.cpp" line="104"/>
+ <location filename="instancemanager.cpp" line="108"/>
<source>Choose Instance to Delete</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="instancemanager.cpp" line="105"/>
+ <location filename="instancemanager.cpp" line="109"/>
<source>Be Careful! Deleting an Instance will remove all your files for that Instance (mods, downloads, profiles, configuration, ...). Custom paths outside of the instance folder for downloads, mods, etc. will be left untoched.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="instancemanager.cpp" line="118"/>
+ <location filename="instancemanager.cpp" line="122"/>
<source>Are you sure?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="instancemanager.cpp" line="119"/>
+ <location filename="instancemanager.cpp" line="123"/>
<source>Are you really sure you want to delete the Instance &quot;%1&quot; with all its files?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="instancemanager.cpp" line="123"/>
+ <location filename="instancemanager.cpp" line="127"/>
<source>Failed to delete Instance</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="instancemanager.cpp" line="124"/>
+ <location filename="instancemanager.cpp" line="128"/>
<source>Could not delete Instance &quot;%1&quot;.
If the folder was still in use, restart MO and try again.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="instancemanager.cpp" line="139"/>
+ <location filename="instancemanager.cpp" line="143"/>
<source>Enter a Name for the new Instance</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="instancemanager.cpp" line="140"/>
+ <location filename="instancemanager.cpp" line="144"/>
<source>Enter a new name or select one from the suggested list:
(This is just a name for the Instance and can be whatever you wish,
the actual game selection will happen on the next screen regardless of chosen name)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="instancemanager.cpp" line="151"/>
- <location filename="instancemanager.cpp" line="224"/>
+ <location filename="instancemanager.cpp" line="155"/>
+ <location filename="instancemanager.cpp" line="232"/>
<source>Canceled</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="instancemanager.cpp" line="157"/>
+ <location filename="instancemanager.cpp" line="161"/>
<source>Invalid instance name</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="instancemanager.cpp" line="158"/>
+ <location filename="instancemanager.cpp" line="162"/>
<source>The instance name &quot;%1&quot; is invalid. Use the name &quot;%2&quot; instead?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="instancemanager.cpp" line="173"/>
+ <location filename="instancemanager.cpp" line="177"/>
<source>The instance &quot;%1&quot; already exists.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="instancemanager.cpp" line="174"/>
+ <location filename="instancemanager.cpp" line="178"/>
<source>Please choose a different instance name, like: &quot;%1 1&quot; .</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="instancemanager.cpp" line="193"/>
+ <location filename="instancemanager.cpp" line="201"/>
<source>Choose Instance</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="instancemanager.cpp" line="194"/>
+ <location filename="instancemanager.cpp" line="202"/>
<source>Each Instance is a full set of MO data files (mods, downloads, profiles, configuration, ...). You can use multiple instances for different games. Instances are stored in Appdata and can be accessed by all MO installations. If your MO folder is writable, you can also store a single instance locally (called a Portable install, and all the MO data files will be inside the installation folder).</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="instancemanager.cpp" line="206"/>
+ <location filename="instancemanager.cpp" line="214"/>
<source>New</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="instancemanager.cpp" line="207"/>
+ <location filename="instancemanager.cpp" line="215"/>
<source>Create a new instance.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="instancemanager.cpp" line="211"/>
+ <location filename="instancemanager.cpp" line="219"/>
<source>Portable</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="instancemanager.cpp" line="212"/>
+ <location filename="instancemanager.cpp" line="220"/>
<source>Use MO folder for data.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="instancemanager.cpp" line="216"/>
+ <location filename="instancemanager.cpp" line="224"/>
<source>Manage Instances</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="instancemanager.cpp" line="217"/>
+ <location filename="instancemanager.cpp" line="225"/>
<source>Delete an Instance.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="instancemanager.cpp" line="270"/>
- <location filename="profile.cpp" line="69"/>
+ <location filename="instancemanager.cpp" line="311"/>
+ <location filename="profile.cpp" line="68"/>
<source>failed to create %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="instancemanager.cpp" line="273"/>
+ <location filename="instancemanager.cpp" line="314"/>
<source>Data directory created</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="instancemanager.cpp" line="274"/>
+ <location filename="instancemanager.cpp" line="315"/>
<source>New data directory created at %1. If you don&apos;t want to store a lot of data there, reconfigure the storage directories via settings.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="loadmechanism.cpp" line="52"/>
- <source>failed to open %1: %2</source>
+ <location filename="loot.cpp" line="260"/>
+ <source>General messages</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="loadmechanism.cpp" line="103"/>
- <location filename="loadmechanism.cpp" line="112"/>
- <source>file not found: %1</source>
+ <location filename="loot.cpp" line="272"/>
+ <source>Plugins</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="loadmechanism.cpp" line="149"/>
- <source>Failed to delete %1</source>
+ <location filename="loot.cpp" line="283"/>
+ <source>No messages.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="loot.cpp" line="331"/>
+ <source>Incompatibilities</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="loot.cpp" line="346"/>
+ <source>Missing masters</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="loot.cpp" line="378"/>
+ <source>Verified clean by %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="loadmechanism.cpp" line="156"/>
- <source>Failed to deactivate script extender loading</source>
+ <location filename="loot.cpp" line="398"/>
+ <source>%1 found %2 ITM record(s), %3 deleted reference(s) and %4 deleted navmesh(es).</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="loadmechanism.cpp" line="176"/>
- <source>Failed to remove %1: %2</source>
+ <location filename="loot.cpp" line="419"/>
+ <location filename="settingsdialogdiagnostics.cpp" line="36"/>
+ <location filename="settingsdialogdiagnostics.cpp" line="60"/>
+ <source>Warning</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="loadmechanism.cpp" line="178"/>
- <location filename="loadmechanism.cpp" line="283"/>
- <source>Failed to rename %1 to %2</source>
+ <location filename="loot.cpp" line="949"/>
+ <source>failed to run loot: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="loadmechanism.cpp" line="185"/>
- <source>Failed to deactivate proxy-dll loading</source>
+ <location filename="lootdialog.cpp" line="15"/>
+ <source>Checking masterlist existence</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="loadmechanism.cpp" line="233"/>
- <location filename="loadmechanism.cpp" line="267"/>
- <location filename="loadmechanism.cpp" line="286"/>
- <source>Failed to copy %1 to %2</source>
+ <location filename="lootdialog.cpp" line="16"/>
+ <source>Updating masterlist</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="loadmechanism.cpp" line="239"/>
- <source>Failed to set up script extender loading</source>
+ <location filename="lootdialog.cpp" line="17"/>
+ <source>Loading lists</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="loadmechanism.cpp" line="264"/>
- <source>Failed to delete old proxy-dll %1</source>
+ <location filename="lootdialog.cpp" line="18"/>
+ <source>Reading plugins</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="loadmechanism.cpp" line="279"/>
- <source>Failed to overwrite %1</source>
+ <location filename="lootdialog.cpp" line="19"/>
+ <source>Sorting plugins</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="loadmechanism.cpp" line="291"/>
- <source>Failed to set up proxy-dll loading</source>
+ <location filename="lootdialog.cpp" line="20"/>
+ <source>Writing loadorder.txt</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="lootdialog.cpp" line="21"/>
+ <source>Parsing loot messages</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="lootdialog.cpp" line="22"/>
+ <source>Done</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="main.cpp" line="99"/>
- <location filename="organizercore.cpp" line="685"/>
+ <location filename="main.cpp" line="105"/>
+ <location filename="organizercore.cpp" line="405"/>
<source>Failed to create &quot;%1&quot;. Your user account probably lacks permission.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="main.cpp" line="298"/>
+ <location filename="main.cpp" line="302"/>
<source>Plugin to handle %1 no longer installed</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="main.cpp" line="308"/>
- <location filename="main.cpp" line="346"/>
- <location filename="main.cpp" line="360"/>
+ <location filename="main.cpp" line="315"/>
+ <location filename="main.cpp" line="357"/>
+ <location filename="main.cpp" line="373"/>
<source>The configured path to the game directory (%1) appears to be a symbolic (or other) link. This setup is incompatible with MO2&apos;s VFS and will not run correctly.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="main.cpp" line="320"/>
+ <location filename="main.cpp" line="330"/>
<source>Could not use configuration settings for game &quot;%1&quot;, path &quot;%2&quot;.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="main.cpp" line="324"/>
- <location filename="main.cpp" line="353"/>
- <location filename="main.cpp" line="375"/>
+ <location filename="main.cpp" line="335"/>
+ <location filename="main.cpp" line="365"/>
+ <location filename="main.cpp" line="390"/>
<source>Please select the installation of %1 to manage</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="main.cpp" line="325"/>
- <location filename="main.cpp" line="354"/>
- <location filename="main.cpp" line="376"/>
+ <location filename="main.cpp" line="336"/>
+ <location filename="main.cpp" line="366"/>
+ <location filename="main.cpp" line="391"/>
<source>Please select the game to manage</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="main.cpp" line="384"/>
+ <location filename="main.cpp" line="402"/>
<source>Canceled finding %1 in &quot;%2&quot;.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="main.cpp" line="385"/>
+ <location filename="main.cpp" line="403"/>
<source>Canceled finding game in &quot;%1&quot;.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="main.cpp" line="391"/>
+ <location filename="main.cpp" line="410"/>
<source>%1 not identified in &quot;%2&quot;. The directory is required to contain the game binary.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="main.cpp" line="399"/>
+ <location filename="main.cpp" line="419"/>
<source>No game identified in &quot;%1&quot;. The directory is required to contain the game binary.&lt;br&gt;&lt;br&gt;&lt;b&gt;These are the games supported by Mod Organizer:&lt;/b&gt;&lt;ul&gt;%2&lt;/ul&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="main.cpp" line="598"/>
+ <location filename="main.cpp" line="620"/>
<source>Please select the game edition you have (MO can&apos;t start the game correctly if this is set incorrectly!)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="main.cpp" line="635"/>
+ <location filename="main.cpp" line="667"/>
<source>failed to start shortcut: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="main.cpp" line="657"/>
+ <location filename="main.cpp" line="695"/>
<source>failed to start application: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="main.cpp" line="805"/>
- <location filename="settings.cpp" line="1186"/>
+ <location filename="main.cpp" line="898"/>
+ <location filename="settingsdialogworkarounds.cpp" line="17"/>
<source>Mod Organizer</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="main.cpp" line="806"/>
+ <location filename="main.cpp" line="899"/>
<source>An instance of Mod Organizer is already running</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="main.cpp" line="821"/>
+ <location filename="main.cpp" line="921"/>
<source>Failed to set up instance</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="1247"/>
+ <location filename="mainwindow.cpp" line="197"/>
+ <source>&lt;Unmanaged&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="mainwindow.cpp" line="1277"/>
<source>Please use &quot;Help&quot; from the toolbar to get usage instructions to all elements</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="1773"/>
- <location filename="mainwindow.cpp" line="5247"/>
+ <location filename="mainwindow.cpp" line="1823"/>
+ <location filename="mainwindow.cpp" line="5115"/>
<source>&lt;Manage...&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="1785"/>
+ <location filename="mainwindow.cpp" line="1835"/>
<source>failed to parse profile %1: %2</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialogesps.cpp" line="314"/>
+ <location filename="modinfodialogesps.cpp" line="315"/>
<source>File Exists</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialogesps.cpp" line="315"/>
+ <location filename="modinfodialogesps.cpp" line="316"/>
<source>A file with that name exists, please enter a new one</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialogesps.cpp" line="334"/>
- <location filename="modinfodialogesps.cpp" line="382"/>
+ <location filename="modinfodialogesps.cpp" line="335"/>
+ <location filename="modinfodialogesps.cpp" line="383"/>
<source>Failed to move file</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="modinfodialogesps.cpp" line="366"/>
+ <location filename="modinfodialogesps.cpp" line="367"/>
<source>Failed to create directory &quot;optional&quot;</source>
<translation type="unfinished"></translation>
</message>
@@ -5904,128 +6192,648 @@ If the folder was still in use, restart MO and try again.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="172"/>
- <source>Failed to start &quot;%1&quot;</source>
+ <location filename="nxmaccessmanager.cpp" line="183"/>
+ <source>Connecting to Nexus...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="175"/>
- <source>Waiting</source>
+ <location filename="nxmaccessmanager.cpp" line="186"/>
+ <source>Waiting for Nexus...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="176"/>
- <source>Please press OK once you&apos;re logged into steam.</source>
+ <location filename="nxmaccessmanager.cpp" line="190"/>
+ <source>Opened Nexus in browser.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="700"/>
- <source>One of the configured MO2 directories (profiles, mods, or overwrite) is on a path containing a symbolic (or other) link. This is incompatible with MO2&apos;s VFS system.</source>
+ <location filename="nxmaccessmanager.cpp" line="191"/>
+ <source>Switch to your browser and accept the request.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="1249"/>
- <source>Select binary</source>
+ <location filename="nxmaccessmanager.cpp" line="194"/>
+ <source>Finished.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="1250"/>
- <source>Binary</source>
+ <location filename="nxmaccessmanager.cpp" line="198"/>
+ <source>No answer from Nexus.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="nxmaccessmanager.cpp" line="199"/>
+ <location filename="nxmaccessmanager.cpp" line="204"/>
+ <source>A firewall might be blocking Mod Organizer.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="nxmaccessmanager.cpp" line="203"/>
+ <source>Nexus closed the connection.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="nxmaccessmanager.cpp" line="207"/>
+ <source>Cancelled.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="nxmaccessmanager.cpp" line="395"/>
+ <source>Failed to request %1</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="nxmaccessmanager.cpp" line="417"/>
+ <location filename="nxmaccessmanager.cpp" line="740"/>
+ <source>Cancelled</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="nxmaccessmanager.cpp" line="463"/>
+ <source>Internal error</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="nxmaccessmanager.cpp" line="498"/>
+ <source>HTTP code %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="plugincontainer.cpp" line="177"/>
+ <location filename="nxmaccessmanager.cpp" line="508"/>
+ <source>Invalid JSON</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="nxmaccessmanager.cpp" line="513"/>
+ <source>Bad response</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="nxmaccessmanager.cpp" line="523"/>
+ <source>API key is empty</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="nxmaccessmanager.cpp" line="545"/>
+ <source>SSL error</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="nxmaccessmanager.cpp" line="550"/>
+ <source>Timed out</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="organizercore.cpp" line="421"/>
+ <source>One of the configured MO2 directories (profiles, mods, or overwrite) is on a path containing a symbolic (or other) link. This is likely to be incompatible with MO2&apos;s virtual filesystem.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="plugincontainer.cpp" line="179"/>
<source>failed to initialize plugin %1: %2</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="plugincontainer.cpp" line="263"/>
+ <location filename="plugincontainer.cpp" line="265"/>
<source>Plugin error</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="plugincontainer.cpp" line="264"/>
+ <location filename="plugincontainer.cpp" line="266"/>
<source>It appears the plugin &quot;%1&quot; failed to load last startup and caused MO to crash. Do you want to disable it?
(Please note: If this is the first time you see this message for this plugin you may want to give it another try. The plugin may be able to recover from the problem)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="pluginlist.cpp" line="542"/>
+ <location filename="pluginlist.cpp" line="571"/>
<source>failed to access %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="pluginlist.cpp" line="556"/>
+ <location filename="pluginlist.cpp" line="585"/>
<source>failed to set file time %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="profilesdialog.cpp" line="96"/>
+ <location filename="processrunner.cpp" line="557"/>
+ <location filename="processrunner.cpp" line="610"/>
+ <location filename="processrunner.cpp" line="756"/>
+ <source>No profile set</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="profilesdialog.cpp" line="102"/>
<source>Before you can use ModOrganizer, you need to create at least one profile. ATTENTION: Run the game at least once before creating a profile!</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settings.cpp" line="1193"/>
- <source>Script Extender</source>
+ <location filename="sanitychecks.cpp" line="113"/>
+ <source>&apos;%1&apos;: file is blocked (%2)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settings.cpp" line="1200"/>
- <source>Proxy DLL</source>
+ <location filename="sanitychecks.cpp" line="206"/>
+ <source>&apos;%1&apos; seems to be missing, an antivirus may have deleted it</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="spawn.cpp" line="146"/>
- <source>failed to spawn &quot;%1&quot;</source>
+ <location filename="sanitychecks.cpp" line="239"/>
+ <source>%1 is loaded. This program is known to cause issues with Mod Organizer, such as freezing or blank windows. Consider uninstalling it.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="spawn.cpp" line="151"/>
- <source>Elevation required</source>
+ <location filename="settings.cpp" line="1288"/>
+ <location filename="settings.cpp" line="1331"/>
+ <source>attempt to store setting for unknown plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="settings.cpp" line="1815"/>
+ <source>Failed</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="settings.cpp" line="1816"/>
+ <source>Failed to start the helper application: %1</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="settingsdialogdiagnostics.cpp" line="34"/>
+ <location filename="settingsdialogdiagnostics.cpp" line="58"/>
+ <source>Debug</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="settingsdialogdiagnostics.cpp" line="35"/>
+ <location filename="settingsdialogdiagnostics.cpp" line="59"/>
+ <source>Info (recommended)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="settingsdialogdiagnostics.cpp" line="57"/>
+ <source>Trace</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="settingsdialogdiagnostics.cpp" line="82"/>
+ <source>Mini (recommended)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="settingsdialogdiagnostics.cpp" line="83"/>
+ <source>Data</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="settingsdialogdiagnostics.cpp" line="84"/>
+ <source>Full</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="settingsdialoggeneral.cpp" line="206"/>
+ <source>Confirm?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="settingsdialoggeneral.cpp" line="207"/>
+ <source>This will reset all the choices you made to dialogs and make them all visible again. Continue?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="spawn.cpp" line="152"/>
- <source>This process requires elevation to run.
-This is a potential security risk so I highly advise you to investigate if
-&quot;%1&quot;
-can be installed to work without elevation.
+ <location filename="settingsdialognexus.cpp" line="116"/>
+ <source>Connected.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="settingsdialognexus.cpp" line="118"/>
+ <source>Not connected.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="settingsdialognexus.cpp" line="229"/>
+ <source>Disconnected.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="settingsdialognexus.cpp" line="254"/>
+ <source>Checking API key...</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="settingsdialognexus.cpp" line="263"/>
+ <source>Received API key.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="settingsdialognexus.cpp" line="288"/>
+ <source>Received user acount information</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="settingsdialognexus.cpp" line="291"/>
+ <source>Linked with Nexus successfully.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="settingsdialognexus.cpp" line="293"/>
+ <source>Failed to set API key</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="settingsdialognexus.cpp" line="342"/>
+ <location filename="settingsdialognexus.cpp" line="353"/>
+ <location filename="settingsdialogworkarounds.cpp" line="128"/>
+ <location filename="spawn.cpp" line="193"/>
+ <location filename="spawn.cpp" line="222"/>
+ <location filename="spawn.cpp" line="301"/>
+ <location filename="spawn.cpp" line="330"/>
+ <location filename="spawn.cpp" line="361"/>
+ <location filename="spawn.cpp" line="388"/>
+ <location filename="spawn.cpp" line="424"/>
+ <location filename="uilocker.cpp" line="361"/>
+ <source>Cancel</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="settingsdialognexus.cpp" line="345"/>
+ <location filename="settingsdialognexus.cpp" line="361"/>
+ <location filename="settingsdialognexus.cpp" line="368"/>
+ <source>Enter API Key Manually</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="settingsdialognexus.cpp" line="350"/>
+ <location filename="settingsdialognexus.cpp" line="358"/>
+ <location filename="settingsdialognexus.cpp" line="365"/>
+ <source>Connect to Nexus</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="settingsdialognexus.cpp" line="391"/>
+ <location filename="settingsdialognexus.cpp" line="392"/>
+ <location filename="settingsdialognexus.cpp" line="393"/>
+ <location filename="settingsdialognexus.cpp" line="394"/>
+ <location filename="settingsdialognexus.cpp" line="395"/>
+ <source>N/A</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="settingsdialogpaths.cpp" line="72"/>
+ <source>Failed to create &quot;%1&quot;, you may not have the necessary permissions. Path remains unchanged.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="settingsdialogpaths.cpp" line="107"/>
+ <source>Select base directory</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="settingsdialogpaths.cpp" line="118"/>
+ <source>Select download directory</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="settingsdialogpaths.cpp" line="129"/>
+ <source>Select mod directory</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="settingsdialogpaths.cpp" line="140"/>
+ <source>Select cache directory</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="settingsdialogpaths.cpp" line="151"/>
+ <source>Select profiles directory</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="settingsdialogpaths.cpp" line="162"/>
+ <source>Select overwrite directory</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="settingsdialogpaths.cpp" line="172"/>
+ <source>Select game executable</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="settingsdialogworkarounds.cpp" line="77"/>
+ <source>Executables Blacklist</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="settingsdialogworkarounds.cpp" line="78"/>
+ <source>Enter one executable per line to be blacklisted from the virtual file system.
+Mods and other virtualized files will not be visible to these executables and
+any executables launched by them.
-Restart Mod Organizer as an elevated process?
-You will be asked if you want to allow helper.exe to make changes to the system. You will need to relaunch the process above manually.</source>
+Example:
+ Chrome.exe
+ Firefox.exe</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="settingsdialogworkarounds.cpp" line="123"/>
+ <location filename="settingsdialogworkarounds.cpp" line="124"/>
+ <location filename="settingsdialogworkarounds.cpp" line="127"/>
+ <source>Restart Mod Organizer</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="settingsdialogworkarounds.cpp" line="125"/>
+ <source>Geometries will be reset to their default values.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="shared/directoryentry.cpp" line="87"/>
+ <source>invalid origin name: </source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="shared/directoryentry.cpp" line="110"/>
+ <source>failed to change name lookup from {} to {}</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="shared/directoryentry.cpp" line="524"/>
+ <source>failed to determine file time</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="shared/directoryentry.cpp" line="546"/>
+ <source>invalid bsa file: </source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="shared/directoryentry.cpp" line="722"/>
+ <source>file &quot;{}&quot; not in directory &quot;{}&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="shared/directoryentry.cpp" line="727"/>
+ <source>file &quot;{}&quot; not in directory &quot;{}&quot;, directory empty</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="shared/directoryentry.cpp" line="851"/>
+ <source>unexpected end of path</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="shared/directoryentry.cpp" line="995"/>
+ <source>invalid file index for remove: {}</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="shared/directoryentry.cpp" line="1009"/>
+ <source>invalid file index for remove (for origin): {}</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="spawn.cpp" line="153"/>
+ <source>This error typically happens because an antivirus has deleted critical files from Mod Organizer&apos;s installation folder or has made them generally inaccessible. Add an exclusion for Mod Organizer&apos;s installation folder in your antivirus, reinstall Mod Organizer and try again.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="spawn.cpp" line="160"/>
+ <source>This error typically happens because an antivirus is preventing Mod Organizer from starting programs. Add an exclusion for Mod Organizer&apos;s installation folder in your antivirus and try again.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="spawn.cpp" line="165"/>
+ <source>The file &apos;%1&apos; does not exist.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="spawn.cpp" line="181"/>
+ <location filename="spawn.cpp" line="182"/>
+ <location filename="spawn.cpp" line="212"/>
+ <location filename="spawn.cpp" line="213"/>
+ <source>Cannot start Steam</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="spawn.cpp" line="183"/>
+ <source>The path to the Steam executable cannot be found. You might try reinstalling Steam.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="spawn.cpp" line="189"/>
+ <location filename="spawn.cpp" line="218"/>
+ <location filename="spawn.cpp" line="326"/>
+ <source>Continue without starting Steam</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="spawn.cpp" line="190"/>
+ <location filename="spawn.cpp" line="219"/>
+ <source>The program may fail to launch.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="spawn.cpp" line="232"/>
+ <source>Cannot launch program</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="spawn.cpp" line="234"/>
+ <location filename="spawn.cpp" line="259"/>
+ <location filename="spawn.cpp" line="278"/>
+ <source>Cannot start %1</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="spawn.cpp" line="257"/>
+ <source>Cannot launch helper</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="spawn.cpp" line="276"/>
+ <location filename="spawn.cpp" line="339"/>
+ <source>Elevation required</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="spawn.cpp" line="281"/>
+ <source>This program is requesting to run as administrator but Mod Organizer itself is not running as administrator. Running programs as administrator is typically unnecessary as long as the game and Mod Organizer have been installed outside &quot;Program Files&quot;.<byte value="xd"/>
+<byte value="xd"/>
+You can restart Mod Organizer as administrator and try launching the program again.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="spawn.cpp" line="297"/>
+ <location filename="spawn.cpp" line="353"/>
+ <source>Restart Mod Organizer as administrator</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="spawn.cpp" line="298"/>
+ <location filename="spawn.cpp" line="354"/>
+ <source>You must allow &quot;helper.exe&quot; to make changes to the system.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="spawn.cpp" line="311"/>
+ <source>Launch Steam</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="spawn.cpp" line="312"/>
+ <source>This program requires Steam</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="spawn.cpp" line="313"/>
+ <source>Mod Organizer has detected that this program likely requires Steam to be running to function properly.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="spawn.cpp" line="323"/>
+ <source>Start Steam</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="spawn.cpp" line="327"/>
+ <location filename="spawn.cpp" line="358"/>
+ <source>The program might fail to run.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="spawn.cpp" line="340"/>
+ <source>Steam is running as administrator</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="spawn.cpp" line="341"/>
+ <source>Running Steam as administrator is typically unnecessary and can cause problems when Mod Organizer itself is not running as administrator.<byte value="xd"/>
+<byte value="xd"/>
+You can restart Mod Organizer as administrator and try launching the program again.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="spawn.cpp" line="357"/>
+ <location filename="spawn.cpp" line="384"/>
+ <location filename="spawn.cpp" line="417"/>
+ <source>Continue</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="spawn.cpp" line="163"/>
- <location filename="spawn.cpp" line="177"/>
- <source>failed to spawn &quot;%1&quot;: %2</source>
+ <location filename="spawn.cpp" line="370"/>
+ <source>Event Log not running</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="statusbar.cpp" line="37"/>
+ <location filename="spawn.cpp" line="371"/>
+ <source>The Event Log service is not running</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="spawn.cpp" line="372"/>
+ <source>The Windows Event Log service is not running. This can prevent USVFS from running properly and your mods may not be recognized by the program being launched.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="spawn.cpp" line="385"/>
+ <location filename="spawn.cpp" line="418"/>
+ <source>Your mods might not work.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="spawn.cpp" line="398"/>
+ <source>Blacklisted program</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="spawn.cpp" line="399"/>
+ <source>The program %1 is blacklisted</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="spawn.cpp" line="401"/>
+ <source>The program you are attempting to launch is blacklisted in the virtual filesystem. This will likely prevent it from seeing any mods, INI files or any other virtualized files.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="spawn.cpp" line="421"/>
+ <source>Change the blacklist</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="spawn.cpp" line="651"/>
+ <source>Waiting</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="spawn.cpp" line="652"/>
+ <source>Please press OK once you&apos;re logged into steam.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="spawn.cpp" line="951"/>
+ <source>Select binary</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="spawn.cpp" line="952"/>
+ <source>Binary</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="statusbar.cpp" line="43"/>
<source>This tracks the number of queued Nexus API requests, as well as the remaining daily and hourly requests. The Nexus API limits you to a pool of requests per day and requests per hour. It is dynamically updated every time a request is completed. If you run out of requests, you will be unable to queue downloads, check updates, parse mod info, or even log in. Both pools must be consumed before this happens.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="statusbar.cpp" line="56"/>
+ <location filename="statusbar.cpp" line="64"/>
<source>Loading...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="texteditor.cpp" line="462"/>
+ <location filename="texteditor.cpp" line="465"/>
<source>&amp;Save</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="texteditor.cpp" line="469"/>
+ <location filename="texteditor.cpp" line="472"/>
<source>&amp;Word wrap</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="texteditor.cpp" line="474"/>
+ <location filename="texteditor.cpp" line="477"/>
<source>&amp;Open in Explorer</source>
<translation type="unfinished"></translation>
</message>
+ <message>
+ <location filename="uilocker.cpp" line="301"/>
+ <source>Mod Organizer is locked while the application is running.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="uilocker.cpp" line="304"/>
+ <source>Mod Organizer is currently running an application.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="uilocker.cpp" line="314"/>
+ <source>The application must run to completion because its output is required.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="uilocker.cpp" line="323"/>
+ <source>Mod Organizer is waiting on application to close before exiting.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="uilocker.cpp" line="341"/>
+ <source>Unlock</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="uilocker.cpp" line="354"/>
+ <source>Exit Now</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>QueryOverwriteDialog</name>
@@ -6068,13 +6876,13 @@ You will be asked if you want to allow helper.exe to make changes to the system.
<context>
<name>QuestionBoxMemory</name>
<message>
- <location filename="../../uibase/src/questionboxmemory.ui" line="88"/>
+ <location filename="../../uibase/src/questionboxmemory.ui" line="106"/>
<source>Remember selection</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="../../uibase/src/questionboxmemory.ui" line="95"/>
- <source>Remember selection only for</source>
+ <location filename="../../uibase/src/questionboxmemory.ui" line="113"/>
+ <source>Remember selection only for %1</source>
<translation type="unfinished"></translation>
</message>
</context>
@@ -6142,531 +6950,557 @@ You will be asked if you want to allow helper.exe to make changes to the system.
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="selfupdater.cpp" line="179"/>
+ <location filename="selfupdater.cpp" line="189"/>
<source>New update available (%1)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="selfupdater.cpp" line="180"/>
+ <location filename="selfupdater.cpp" line="190"/>
<source>Do you want to install update? All your mods and setup will be left untouched.
Select Show Details option to see the full change-log.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="selfupdater.cpp" line="184"/>
+ <location filename="selfupdater.cpp" line="194"/>
<source>Install</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="selfupdater.cpp" line="201"/>
+ <location filename="selfupdater.cpp" line="211"/>
<source>Download failed</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="selfupdater.cpp" line="202"/>
+ <location filename="selfupdater.cpp" line="212"/>
<source>Failed to find correct download, please try again later.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="selfupdater.cpp" line="217"/>
+ <location filename="selfupdater.cpp" line="227"/>
<source>Update</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="selfupdater.cpp" line="218"/>
+ <location filename="selfupdater.cpp" line="228"/>
<source>Download in progress</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="selfupdater.cpp" line="309"/>
+ <location filename="selfupdater.cpp" line="319"/>
<source>Download failed: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="selfupdater.cpp" line="320"/>
+ <location filename="selfupdater.cpp" line="330"/>
<source>Failed to install update: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="selfupdater.cpp" line="338"/>
- <source>Failed to start %1</source>
+ <location filename="selfupdater.cpp" line="349"/>
+ <source>Failed to start %1: %2</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="selfupdater.cpp" line="346"/>
+ <location filename="selfupdater.cpp" line="359"/>
<source>Error</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
- <name>Settings</name>
- <message>
- <location filename="settings.cpp" line="146"/>
- <source>Failed</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="settings.cpp" line="146"/>
- <source>Failed to start the helper application</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="settings.cpp" line="566"/>
- <location filename="settings.cpp" line="585"/>
- <source>attempt to store setting for unknown plugin &quot;%1&quot;</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="settings.cpp" line="783"/>
- <source>Restart Mod Organizer?</source>
- <translation type="unfinished"></translation>
- </message>
+ <name>SettingsDialog</name>
<message>
- <location filename="settings.cpp" line="784"/>
- <source>In order to finish configuration changes, MO must be restarted.
-Restart it now?</source>
+ <location filename="settingsdialog.ui" line="14"/>
+ <source>Settings</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settings.cpp" line="967"/>
- <source>Error</source>
+ <location filename="settingsdialog.ui" line="24"/>
+ <source>General</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settings.cpp" line="968"/>
- <source>Failed to create &quot;%1&quot;, you may not have the necessary permission. path remains unchanged.</source>
+ <location filename="settingsdialog.ui" line="43"/>
+ <source>User Interface</source>
<translation type="unfinished"></translation>
</message>
-</context>
-<context>
- <name>SettingsDialog</name>
<message>
- <location filename="settingsdialog.ui" line="14"/>
- <source>Settings</source>
+ <location filename="settingsdialog.ui" line="64"/>
+ <source>Language</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="24"/>
- <source>General</source>
+ <location filename="settingsdialog.ui" line="71"/>
+ <source>Style</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="32"/>
- <source>Language</source>
+ <location filename="settingsdialog.ui" line="78"/>
+ <location filename="settingsdialog.ui" line="81"/>
+ <source>Visual theme of the user interface.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="39"/>
- <source>The display language</source>
+ <location filename="settingsdialog.ui" line="88"/>
+ <source>Explore...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="42"/>
- <source>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;
-&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;
-p, li { white-space: pre-wrap; }
-&lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:&apos;MS Shell Dlg 2&apos;; font-size:8.25pt; font-weight:400; font-style:normal;&quot;&gt;
-&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-size:8pt;&quot;&gt;The display language. This will only displaye languages for which you have a translation installed.&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <location filename="settingsdialog.ui" line="95"/>
+ <location filename="settingsdialog.ui" line="98"/>
+ <source>The language of the user interface.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="57"/>
- <source>Style</source>
+ <location filename="settingsdialog.ui" line="105"/>
+ <source>https://www.transifex.com/mod-organizer-2-team/mod-organizer-2/</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="64"/>
- <source>graphical style</source>
+ <location filename="settingsdialog.ui" line="108"/>
+ <source>&lt;a href=&quot;https://www.transifex.com/mod-organizer-2-team/mod-organizer-2/&quot;&gt;Help translate Mod Organizer&lt;/a&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="67"/>
- <source>graphical style of the MO user interface</source>
+ <location filename="settingsdialog.ui" line="121"/>
+ <location filename="settingsdialog.ui" line="124"/>
+ <source>Dialogs will always be centered on the main window, but will remember their size.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="76"/>
- <source>Update to non-stable releases.</source>
+ <location filename="settingsdialog.ui" line="127"/>
+ <source>Always center dialogs</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="79"/>
- <source>If this is enabled, the integrated update mechanism will notify of all releases, including pre-releases (alphas, betas).
-
-Please use this only if you&apos;re sufficiently tech-savvy to investigate issues, look for known problems in the issue tracker and create meaningful reports.
-
-If you use pre-releases, never contact me directly by e-mail or via private messages!</source>
+ <location filename="settingsdialog.ui" line="134"/>
+ <source>Show confirmation when changing instance</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="86"/>
- <source>Install Pre-releases (Betas)</source>
+ <location filename="settingsdialog.ui" line="141"/>
+ <source>Whether double-clicking on a file opens the preview window or launches the program associated with it. This applies to the Data tab as well as the Conflicts and Filetree tabs in the mod info window.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="93"/>
- <source>User interface</source>
+ <location filename="settingsdialog.ui" line="144"/>
+ <source>Open previews on double-click</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="99"/>
- <source>Colors</source>
+ <location filename="settingsdialog.ui" line="157"/>
+ <location filename="settingsdialog.ui" line="160"/>
+ <source>Reset all choices made in dialogs.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="105"/>
- <location filename="settingsdialog.ui" line="108"/>
- <source>When this is enabled, the color defined for a separator will be shown on the mod list scrollbar at the location of the separator. This can be useful for quick navigation between separator sections or to a specific separator section.</source>
+ <location filename="settingsdialog.ui" line="163"/>
+ <source>Reset Dialog Choices</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="111"/>
- <source>Show mod list separator colors on the scrollbar</source>
+ <location filename="settingsdialog.ui" line="170"/>
+ <location filename="settingsdialog.ui" line="173"/>
+ <source>Modify the categories available to arrange your mods.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="121"/>
- <source>Plugin is Contained in selected Mod</source>
+ <location filename="settingsdialog.ui" line="176"/>
+ <source>Configure Mod Categories</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="128"/>
- <source>Is overwritten (loose files)</source>
+ <location filename="settingsdialog.ui" line="199"/>
+ <source>Download List</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="135"/>
- <source>Is overwriting (loose files)</source>
+ <location filename="settingsdialog.ui" line="205"/>
+ <location filename="settingsdialog.ui" line="208"/>
+ <source>Show meta information instead of file names in the download list.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="142"/>
- <source>Reset Colors</source>
+ <location filename="settingsdialog.ui" line="211"/>
+ <source>Show Meta Information</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="149"/>
- <source>Mod Contains selected Plugin</source>
+ <location filename="settingsdialog.ui" line="218"/>
+ <location filename="settingsdialog.ui" line="221"/>
+ <source>Make the download list more compact.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="156"/>
- <source>Is overwritten (archive files)</source>
+ <location filename="settingsdialog.ui" line="224"/>
+ <source>Compact List</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="163"/>
- <source>Is overwriting (archive files)</source>
+ <location filename="settingsdialog.ui" line="247"/>
+ <source>Colors</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="173"/>
- <location filename="settingsdialog.ui" line="176"/>
- <source>Modify the categories available to arrange your mods.</source>
+ <location filename="settingsdialog.ui" line="299"/>
+ <location filename="settingsdialog.ui" line="302"/>
+ <source>Colors set on separators will also be shown in the mod list scrollbar at the location of the separator. This can be useful for quickly navigating to a specific separator.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="179"/>
- <source>Configure Mod Categories</source>
+ <location filename="settingsdialog.ui" line="305"/>
+ <source>Show mod list separator colors on the scrollbar</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="192"/>
- <source>Reset stored information from dialogs.</source>
+ <location filename="settingsdialog.ui" line="328"/>
+ <location filename="settingsdialog.ui" line="331"/>
+ <source>Reset all colors to their default value.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="195"/>
- <source>This will make all dialogs show up again where you checked the &quot;Remember selection&quot;-box.</source>
+ <location filename="settingsdialog.ui" line="334"/>
+ <source>Reset Colors</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="198"/>
- <source>Reset Dialogs</source>
+ <location filename="settingsdialog.ui" line="347"/>
+ <source>Updates</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="205"/>
- <source>If checked, the download interface will be more compact.</source>
+ <location filename="settingsdialog.ui" line="353"/>
+ <location filename="settingsdialog.ui" line="356"/>
+ <source>Check for Mod Organizer updates on Github on startup.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="208"/>
- <source>Compact Download Interface</source>
+ <location filename="settingsdialog.ui" line="359"/>
+ <source>Check for updates</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="228"/>
- <source>If checked, the download list will display meta information instead of file names.</source>
+ <location filename="settingsdialog.ui" line="366"/>
+ <location filename="settingsdialog.ui" line="369"/>
+ <source>Update to non-stable releases.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="231"/>
- <source>Download Meta Information</source>
+ <location filename="settingsdialog.ui" line="372"/>
+ <source>Install Pre-releases (Betas)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="242"/>
+ <location filename="settingsdialog.ui" line="396"/>
<source>Paths</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="250"/>
- <location filename="settingsdialog.ui" line="267"/>
- <location filename="settingsdialog.ui" line="364"/>
- <location filename="settingsdialog.ui" line="422"/>
+ <location filename="settingsdialog.ui" line="404"/>
+ <location filename="settingsdialog.ui" line="421"/>
+ <location filename="settingsdialog.ui" line="518"/>
+ <location filename="settingsdialog.ui" line="576"/>
<source>...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="277"/>
+ <location filename="settingsdialog.ui" line="431"/>
<source>Caches</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="284"/>
+ <location filename="settingsdialog.ui" line="438"/>
<source>Overwrite</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="291"/>
- <location filename="settingsdialog.ui" line="294"/>
+ <location filename="settingsdialog.ui" line="445"/>
+ <location filename="settingsdialog.ui" line="448"/>
<source>Directory where downloads are stored.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="311"/>
+ <location filename="settingsdialog.ui" line="465"/>
<source>Downloads</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="331"/>
+ <location filename="settingsdialog.ui" line="485"/>
<source>Profiles</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="354"/>
+ <location filename="settingsdialog.ui" line="508"/>
<source>Directory where mods are stored.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="357"/>
+ <location filename="settingsdialog.ui" line="511"/>
<source>Directory where mods are stored. Please note that changing this will break all associations of profiles with mods that don&apos;t exist in the new location (with the same name).</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="381"/>
+ <location filename="settingsdialog.ui" line="535"/>
<source>Mods</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="401"/>
+ <location filename="settingsdialog.ui" line="555"/>
<source>Managed Game</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="408"/>
+ <location filename="settingsdialog.ui" line="562"/>
<source>Base Directory</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="415"/>
+ <location filename="settingsdialog.ui" line="569"/>
<source>Use %BASE_DIR% to refer to the Base Directory.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="444"/>
+ <location filename="settingsdialog.ui" line="598"/>
<source>Important: All directories have to be writable!</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="452"/>
- <location filename="settingsdialog.ui" line="468"/>
+ <location filename="settingsdialog.ui" line="606"/>
<source>Nexus</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="458"/>
- <source>Allows Mod Organizer to connect to the Nexus for downloading mods, checking for updates, and other such things.</source>
+ <location filename="settingsdialog.ui" line="627"/>
+ <source>Nexus Account</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="461"/>
- <source>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;
-&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;
-p, li { white-space: pre-wrap; }
-&lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:&apos;MS Shell Dlg 2&apos;; font-size:8pt; font-weight:400; font-style:normal;&quot;&gt;
-&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;Allows Mod Organizer to connect to the Nexus for downloading mods, checking for updates, and other such things. Clicking &amp;quot;Connect to Nexus&amp;quot; will open a Nexus webpage to authorise Mod Organizer. You will need to be logged into your Nexus account. The authorisation is stored in the Windows Credential Manager. Your Nexus username and password are not required or stored by Mod Organizer.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <location filename="settingsdialog.ui" line="636"/>
+ <source>User ID:</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="476"/>
- <source>Connect to Nexus</source>
+ <location filename="settingsdialog.ui" line="646"/>
+ <source>id</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="506"/>
- <source>Manually enter the API key and try to login</source>
+ <location filename="settingsdialog.ui" line="653"/>
+ <source>Name:</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="509"/>
- <source>Enter API Key Manually</source>
+ <location filename="settingsdialog.ui" line="660"/>
+ <source>name</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="516"/>
- <source>Clear the stored Nexus API key and force reauthorization.</source>
+ <location filename="settingsdialog.ui" line="667"/>
+ <source>Account:</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="519"/>
- <source>Disconnect from Nexus</source>
+ <location filename="settingsdialog.ui" line="674"/>
+ <source>account</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="530"/>
- <source>Remove cache and cookies.</source>
+ <location filename="settingsdialog.ui" line="684"/>
+ <source>Statistics</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="533"/>
- <source>Clear Cache</source>
+ <location filename="settingsdialog.ui" line="693"/>
+ <source>Daily requests:</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="564"/>
+ <location filename="settingsdialog.ui" line="700"/>
+ <source>daily requests</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="settingsdialog.ui" line="707"/>
+ <source>Hourly requests:</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="settingsdialog.ui" line="714"/>
+ <source>hourly requests</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="settingsdialog.ui" line="727"/>
+ <source>Nexus Connection</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="settingsdialog.ui" line="748"/>
+ <source>Connect to Nexus</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="settingsdialog.ui" line="755"/>
+ <source>Manually enter the API key and try to login</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="settingsdialog.ui" line="758"/>
+ <source>Enter API Key Manually</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="settingsdialog.ui" line="765"/>
+ <source>Clear the stored Nexus API key and force reauthorization.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="settingsdialog.ui" line="768"/>
+ <source>Disconnect from Nexus</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="settingsdialog.ui" line="843"/>
<source>Disable automatic internet features</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="567"/>
+ <location filename="settingsdialog.ui" line="846"/>
<source>Disable automatic internet features. This does not affect features that are explicitly invoked by the user (like checking mods for updates, endorsing, opening the web browser)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="570"/>
+ <location filename="settingsdialog.ui" line="849"/>
<source>Offline Mode</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="577"/>
+ <location filename="settingsdialog.ui" line="856"/>
<source>Use a proxy for network connections.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="580"/>
+ <location filename="settingsdialog.ui" line="859"/>
<source>Use a proxy for network connections. This uses the system-wide settings which can be configured in Internet Explorer. Please note that MO will start up a few seconds slower on some systems when using a proxy.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="583"/>
+ <location filename="settingsdialog.ui" line="862"/>
<source>Use HTTP Proxy (Uses System Settings)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="590"/>
+ <location filename="settingsdialog.ui" line="869"/>
<source>Endorsement Integration</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="600"/>
- <location filename="settingsdialog.ui" line="603"/>
- <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;By default, a counter is displayed under the mod list. This informs the user of their remaining API requests. The Nexus API becomes unusable once these API requests run out. Checking this option will hide that counter.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <location filename="settingsdialog.ui" line="879"/>
+ <location filename="settingsdialog.ui" line="882"/>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;By default, a counter is displayed in the bottom right corner. This informs the user of their remaining API requests. The Nexus API becomes unusable once these API requests run out. Checking this option will hide that counter.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="606"/>
+ <location filename="settingsdialog.ui" line="885"/>
<source>Hide API Request Counter</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="617"/>
+ <location filename="settingsdialog.ui" line="896"/>
<source>Associate with &quot;Download with manager&quot; links</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="646"/>
+ <location filename="settingsdialog.ui" line="903"/>
+ <source>Remove cache and cookies.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="settingsdialog.ui" line="906"/>
+ <source>Clear Cache</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="settingsdialog.ui" line="939"/>
<source>Known Servers (updated on download)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="667"/>
+ <location filename="settingsdialog.ui" line="960"/>
<source>Preferred Servers (Drag &amp; Drop)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="702"/>
+ <location filename="settingsdialog.ui" line="982"/>
<source>Steam</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="708"/>
+ <location filename="settingsdialog.ui" line="988"/>
<source>Username</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="718"/>
- <source>Password</source>
+ <location filename="settingsdialog.ui" line="1014"/>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;If you save your steam user ID and password here, they will be used when logging into steam.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="748"/>
- <source>If you save your steam user ID and password here, they will be used when logging into steam. Note, however, your password will be stored unencrypted, so make sure your computer is secure.</source>
+ <location filename="settingsdialog.ui" line="1024"/>
+ <source>Password</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="775"/>
+ <location filename="settingsdialog.ui" line="1039"/>
<source>Plugins</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="800"/>
+ <location filename="settingsdialog.ui" line="1064"/>
<source>Author:</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="814"/>
+ <location filename="settingsdialog.ui" line="1078"/>
<source>Version:</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="828"/>
+ <location filename="settingsdialog.ui" line="1092"/>
<source>Description:</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="866"/>
+ <location filename="settingsdialog.ui" line="1130"/>
<source>Key</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="871"/>
+ <location filename="settingsdialog.ui" line="1135"/>
<source>Value</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="883"/>
+ <location filename="settingsdialog.ui" line="1147"/>
<source>Blacklisted Plugins (use &lt;del&gt; to remove):</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="894"/>
+ <location filename="settingsdialog.ui" line="1158"/>
<source>Workarounds</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="902"/>
+ <location filename="settingsdialog.ui" line="1166"/>
<source>Steam App ID</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="922"/>
+ <location filename="settingsdialog.ui" line="1186"/>
<source>The Steam AppID for your game</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="925"/>
+ <location filename="settingsdialog.ui" line="1189"/>
<source>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;
&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;
p, li { white-space: pre-wrap; }
@@ -6682,17 +7516,17 @@ p, li { white-space: pre-wrap; }
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="956"/>
+ <location filename="settingsdialog.ui" line="1220"/>
<source>Load Mechanism</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="976"/>
+ <location filename="settingsdialog.ui" line="1240"/>
<source>Select loading mechanism. See help for details.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="979"/>
+ <location filename="settingsdialog.ui" line="1243"/>
<source>Mod Organizer needs a dll to be injected into the game so all mods are visible to it.
There are several means to do this:
*Mod Organizer* (default) In this mode the Mod Organizer itself injects the dll. The disadvantage is that you always have to start the game through MO or a link created by it.
@@ -6703,28 +7537,28 @@ If you use the Steam version of Oblivion the default will NOT work. In this case
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="999"/>
+ <location filename="settingsdialog.ui" line="1263"/>
<source>Enforces that inactive ESPs and ESMs are never loaded.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="1002"/>
+ <location filename="settingsdialog.ui" line="1266"/>
<source>It seems that the Games occasionally load ESP or ESM files even if they haven&apos;t been activated as plugins.
I don&apos;t yet know what the circumstances are, but user reports imply it is in some cases unwanted. If this is checked, ESPs and ESMs not checked in the List are invisible to the game and can not be loaded.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="1006"/>
+ <location filename="settingsdialog.ui" line="1270"/>
<source>Hide inactive ESPs/ESMs</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="1013"/>
+ <location filename="settingsdialog.ui" line="1277"/>
<source>Disable this to no longer display mods installed outside MO in the mod list (left pane). Assets from those mods will then be treated as having lowest mod priority together with the original game content.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="1016"/>
+ <location filename="settingsdialog.ui" line="1280"/>
<source>By default Mod Organizer will display esp+bsa bundles installed with foreign tools as mods (left pane). This allows you to control their priority in relation to other mods. This is particularly useful if you also use Steam Workshop to install mods.
However, if you installed loose file mods outside MO which conflict with BSAs also installed outside MO those conflicts can&apos;t be resolved correctly.
@@ -6732,66 +7566,66 @@ If you disable this feature, MO will only display official DLCs this way. Please
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="1022"/>
+ <location filename="settingsdialog.ui" line="1286"/>
<source>Display mods installed outside MO</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="1032"/>
+ <location filename="settingsdialog.ui" line="1296"/>
<source>If checked, files (i.e. esps, esms and bsas) belonging to the core game can not be disabled in the UI. (default: on)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="1035"/>
+ <location filename="settingsdialog.ui" line="1299"/>
<source>If checked, files (i.e. esps, esms and bsas) belonging to the core game can not be disabled in the UI. (default: on)
Uncheck this if you want to use Mod Organizer with total conversions (like Nehrim) but be aware that the game will crash if required files are not enabled.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="1039"/>
+ <location filename="settingsdialog.ui" line="1303"/>
<source>Force-enable game files</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="1049"/>
- <location filename="settingsdialog.ui" line="1052"/>
+ <location filename="settingsdialog.ui" line="1313"/>
+ <location filename="settingsdialog.ui" line="1316"/>
<source>Disable this to prevent the GUI from being locked when running an executable. This may result in abnormal behavior.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="1055"/>
+ <location filename="settingsdialog.ui" line="1319"/>
<source>Lock GUI when running executable</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="1065"/>
+ <location filename="settingsdialog.ui" line="1329"/>
<source>Enable parsing of Archives. This is an Experimental Feature. Has negative effects on performance and known incorrectness.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="1068"/>
+ <location filename="settingsdialog.ui" line="1332"/>
<source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;By default, MO will parse archive files (BSA, BA2) to calculate conflicts between the contents of the archive files and other loose files. This process has a noticeable cost in performance.&lt;/p&gt;&lt;p&gt;This feature should not be confused with the archive management feature offered by MO1. MO2 will only show conflicts with archives and will NOT load them into the game or program.&lt;/p&gt;&lt;p&gt;If you disable this feature, MO will only display conflicts between loose files.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="1071"/>
+ <location filename="settingsdialog.ui" line="1335"/>
<source>Enable parsing of Archives (Experimental Feature)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="1083"/>
- <location filename="settingsdialog.ui" line="1087"/>
+ <location filename="settingsdialog.ui" line="1347"/>
+ <location filename="settingsdialog.ui" line="1351"/>
<source>For Skyrim, this can be used instead of Archive Invalidation. It should make AI redundant for all Profiles.
For the other games this is not a sufficient replacement for AI!</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="1091"/>
+ <location filename="settingsdialog.ui" line="1355"/>
<source>Back-date BSAs</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="1102"/>
+ <location filename="settingsdialog.ui" line="1366"/>
<source>Add executables to the blacklist to prevent them from
accessing the virtual file system. This is useful to prevent
unintended programs from being hooked. Hooking unintended
@@ -6800,81 +7634,81 @@ programs you are intentionally running.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="1109"/>
+ <location filename="settingsdialog.ui" line="1373"/>
<source>Add executables to the blacklist to prevent them from accessing the virtual file system. This is useful to prevent unintended programs from being hooked. Hooking unintended programs may affect the execution of these programs or the programs you are intentionally running.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="1112"/>
+ <location filename="settingsdialog.ui" line="1376"/>
<source>Configure Executables Blacklist</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="1122"/>
- <location filename="settingsdialog.ui" line="1125"/>
+ <location filename="settingsdialog.ui" line="1386"/>
+ <location filename="settingsdialog.ui" line="1389"/>
<source>Resets the window geometries for all windows. This can be useful if a window becomes too small or too large, if a column becomes too thin or too wide, and in similar situations.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="1128"/>
+ <location filename="settingsdialog.ui" line="1392"/>
<source>Reset Window Geometries</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="1151"/>
+ <location filename="settingsdialog.ui" line="1412"/>
<source>These are workarounds for problems with Mod Organizer. Please make sure you read the help text before changing anything here.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="1162"/>
+ <location filename="settingsdialog.ui" line="1423"/>
<source>Diagnostics</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="1170"/>
- <source>Max Dumps To Keep</source>
+ <location filename="settingsdialog.ui" line="1445"/>
+ <source>Hint: right click link and copy link location</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="1190"/>
- <source>Maximum number of crash dumps to keep on disk. Use 0 for unlimited.</source>
+ <location filename="settingsdialog.ui" line="1448"/>
+ <source>
+ Logs and crash dumps are stored under your current instance in the &lt;a href=&quot;LOGS_FULL_PATH&quot;&gt;LOGS_DIR&lt;/a&gt;
+ and &lt;a href=&quot;DUMPS_FULL_PATH&quot;&gt;DUMPS_DIR&lt;/a&gt; folders.
+ Sending logs and/or crash dumps to the developers can help investigate issues.
+ It is recommended to compress large log and dmp files before sending.
+ </source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="1193"/>
- <source>
- Maximum number of crash dumps to keep on disk. Use 0 for unlimited.
- Set &quot;Crash Dumps&quot; above to None to disable crash dump collection.
- </source>
+ <location filename="settingsdialog.ui" line="1474"/>
+ <source>Log Level</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="1205"/>
- <source>Hint: right click link and copy link location</source>
+ <location filename="settingsdialog.ui" line="1481"/>
+ <source>Decides the amount of data printed to &quot;ModOrganizer.log&quot;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="1208"/>
+ <location filename="settingsdialog.ui" line="1484"/>
<source>
- Logs and crash dumps are stored under your current instance in the &lt;a href=&quot;LOGS_FULL_PATH&quot;&gt;LOGS_DIR&lt;/a&gt;
- and &lt;a href=&quot;DUMPS_FULL_PATH&quot;&gt;DUMPS_DIR&lt;/a&gt; folders.
- Sending logs and/or crash dumps to the developers can help investigate issues.
- It is recommended to compress large log and dmp files before sending.
- </source>
+ Decides the amount of data printed to &quot;ModOrganizer.log&quot;.
+ &quot;Debug&quot; produces very useful information for finding problems. There is usually no noteworthy performance impact but the file may become rather large. If this is a problem you may prefer the &quot;Info&quot; level for regular use. On the &quot;Error&quot; level the log file usually remains empty.
+ </source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="1225"/>
+ <location filename="settingsdialog.ui" line="1494"/>
<source>Crash Dumps</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="1232"/>
+ <location filename="settingsdialog.ui" line="1501"/>
<source>Decides which type of crash dumps are collected when injected processes crash.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="1235"/>
+ <location filename="settingsdialog.ui" line="1504"/>
<source>
Decides which type of crash dumps are collected when injected processes crash.
&quot;None&quot; Disables the generation of crash dumps by MO.
@@ -6885,194 +7719,130 @@ programs you are intentionally running.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="1245"/>
- <source>None</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="settingsdialog.ui" line="1250"/>
- <source>Mini (recommended)</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="settingsdialog.ui" line="1255"/>
- <source>Data</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="settingsdialog.ui" line="1260"/>
- <source>Full</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="settingsdialog.ui" line="1288"/>
- <source>Log Level</source>
+ <location filename="settingsdialog.ui" line="1517"/>
+ <source>Max Dumps To Keep</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="1295"/>
- <source>Decides the amount of data printed to &quot;ModOrganizer.log&quot;</source>
+ <location filename="settingsdialog.ui" line="1524"/>
+ <source>Maximum number of crash dumps to keep on disk. Use 0 for unlimited.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="1298"/>
+ <location filename="settingsdialog.ui" line="1527"/>
<source>
- Decides the amount of data printed to &quot;ModOrganizer.log&quot;.
- &quot;Debug&quot; produces very useful information for finding problems. There is usually no noteworthy performance impact but the file may become rather large. If this is a problem you may prefer the &quot;Info&quot; level for regular use. On the &quot;Error&quot; level the log file usually remains empty.
+ Maximum number of crash dumps to keep on disk. Use 0 for unlimited.
+ Set &quot;Crash Dumps&quot; above to None to disable crash dump collection.
</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="1305"/>
- <source>Debug</source>
+ <location filename="settingsdialog.ui" line="1537"/>
+ <source>LOOT Log Level</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.ui" line="1310"/>
- <source>Info (recommended)</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="settingsdialog.ui" line="1315"/>
- <source>Warning</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="settingsdialog.ui" line="1320"/>
- <location filename="settingsdialog.cpp" line="492"/>
- <source>Error</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="settingsdialog.cpp" line="170"/>
+ <location filename="settingsdialog.cpp" line="123"/>
<source>Confirm</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.cpp" line="171"/>
+ <location filename="settingsdialog.cpp" line="124"/>
<source>Changing the mod directory affects all your profiles! Mods not present (or named differently) in the new location will be disabled in all profiles. There is no way to undo this unless you backed up your profiles manually. Proceed?</source>
<translation type="unfinished"></translation>
</message>
+</context>
+<context>
+ <name>SingleInstance</name>
<message>
- <location filename="settingsdialog.cpp" line="207"/>
- <source>Executables Blacklist</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="settingsdialog.cpp" line="208"/>
- <source>Enter one executable per line to be blacklisted from the virtual file system.
-Mods and other virtualized files will not be visible to these executables and
-any executables launched by them.
-
-Example:
- Chrome.exe
- Firefox.exe</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="settingsdialog.cpp" line="241"/>
- <source>Select base directory</source>
+ <location filename="singleinstance.cpp" line="52"/>
+ <source>SHM error: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.cpp" line="252"/>
- <source>Select download directory</source>
+ <location filename="singleinstance.cpp" line="86"/>
+ <source>failed to connect to running instance: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.cpp" line="263"/>
- <source>Select mod directory</source>
+ <location filename="singleinstance.cpp" line="92"/>
+ <source>failed to communicate with running instance: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.cpp" line="274"/>
- <source>Select cache directory</source>
+ <location filename="singleinstance.cpp" line="119"/>
+ <source>failed to receive data from secondary instance: %1</source>
<translation type="unfinished"></translation>
</message>
+</context>
+<context>
+ <name>SyncOverwriteDialog</name>
<message>
- <location filename="settingsdialog.cpp" line="285"/>
- <source>Select profiles directory</source>
+ <location filename="syncoverwritedialog.ui" line="14"/>
+ <source>Sync Overwrite</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.cpp" line="296"/>
- <source>Select overwrite directory</source>
+ <location filename="syncoverwritedialog.ui" line="27"/>
+ <source>Name</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.cpp" line="306"/>
- <source>Select game executable</source>
+ <location filename="syncoverwritedialog.ui" line="32"/>
+ <source>Sync To</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.cpp" line="385"/>
- <source>Confirm?</source>
+ <location filename="syncoverwritedialog.cpp" line="97"/>
+ <source>&lt;don&apos;t sync&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.cpp" line="386"/>
- <source>This will make all dialogs show up again where you checked the &quot;Remember selection&quot;-box. Continue?</source>
+ <location filename="syncoverwritedialog.cpp" line="149"/>
+ <source>failed to remove %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="settingsdialog.cpp" line="493"/>
- <source>Failed to retrieve a Nexus API key! Please try again. A browser window should open asking you to authorize.</source>
+ <location filename="syncoverwritedialog.cpp" line="151"/>
+ <source>failed to move %1 to %2</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
- <name>SingleInstance</name>
- <message>
- <location filename="singleinstance.cpp" line="51"/>
- <source>SHM error: %1</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="singleinstance.cpp" line="85"/>
- <source>failed to connect to running instance: %1</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="singleinstance.cpp" line="91"/>
- <source>failed to communicate with running instance: %1</source>
- <translation type="unfinished"></translation>
- </message>
+ <name>TaskDialog</name>
<message>
- <location filename="singleinstance.cpp" line="103"/>
- <source>failed to receive data from secondary instance: %1</source>
+ <location filename="../../uibase/src/taskdialog.ui" line="20"/>
+ <source>Dialog</source>
<translation type="unfinished"></translation>
</message>
-</context>
-<context>
- <name>SyncOverwriteDialog</name>
<message>
- <location filename="syncoverwritedialog.ui" line="14"/>
- <source>Sync Overwrite</source>
+ <location filename="../../uibase/src/taskdialog.ui" line="56"/>
+ <source>icon</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="syncoverwritedialog.ui" line="27"/>
- <source>Name</source>
+ <location filename="../../uibase/src/taskdialog.ui" line="102"/>
+ <source>dummy main text</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="syncoverwritedialog.ui" line="32"/>
- <source>Sync To</source>
+ <location filename="../../uibase/src/taskdialog.ui" line="146"/>
+ <source>dummy content text</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="syncoverwritedialog.cpp" line="96"/>
- <source>&lt;don&apos;t sync&gt;</source>
+ <location filename="../../uibase/src/taskdialog.ui" line="206"/>
+ <source>dummy button</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="syncoverwritedialog.cpp" line="148"/>
- <source>failed to remove %1</source>
+ <location filename="../../uibase/src/taskdialog.ui" line="234"/>
+ <source>dummy checkbox</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="syncoverwritedialog.cpp" line="150"/>
- <source>failed to move %1 to %2</source>
+ <location filename="../../uibase/src/taskdialog.ui" line="320"/>
+ <source>Details</source>
<translation type="unfinished"></translation>
</message>
</context>
@@ -7162,22 +7932,22 @@ On Windows XP:
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="transfersavesdialog.cpp" line="119"/>
+ <location filename="transfersavesdialog.cpp" line="120"/>
<source>Characters for profile %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="transfersavesdialog.cpp" line="159"/>
+ <location filename="transfersavesdialog.cpp" line="160"/>
<source>Overwrite</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="transfersavesdialog.cpp" line="160"/>
+ <location filename="transfersavesdialog.cpp" line="161"/>
<source>Overwrite the file &quot;%1&quot;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="transfersavesdialog.cpp" line="319"/>
+ <location filename="transfersavesdialog.cpp" line="320"/>
<source>Confirm</source>
<translation type="unfinished"></translation>
</message>
@@ -7185,36 +7955,37 @@ On Windows XP:
<context>
<name>UsvfsConnector</name>
<message>
- <location filename="usvfsconnector.cpp" line="163"/>
+ <location filename="usvfsconnector.cpp" line="185"/>
<source>Preparing vfs</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
- <name>WaitingOnCloseDialog</name>
+ <name>ValidationProgressDialog</name>
<message>
- <location filename="waitingonclosedialog.ui" line="14"/>
- <source>Waiting for virtualized processes</source>
+ <location filename="validationprogressdialog.ui" line="14"/>
+ <source>Validating Nexus Connection</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="waitingonclosedialog.ui" line="20"/>
- <source>This dialog should disappear automatically if the application/game is done.</source>
+ <location filename="validationprogressdialog.ui" line="35"/>
+ <location filename="nxmaccessmanager.cpp" line="142"/>
+ <source>Connecting to Nexus...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="waitingonclosedialog.ui" line="23"/>
- <source>Virtualized processes are still running, it is prefered to keep MO running until they are finished.</source>
+ <location filename="validationprogressdialog.ui" line="83"/>
+ <source>Cancel</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="waitingonclosedialog.ui" line="77"/>
- <source>Close Now</source>
+ <location filename="validationprogressdialog.ui" line="103"/>
+ <source>Hide</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="waitingonclosedialog.ui" line="97"/>
- <source>Cancel</source>
+ <location filename="nxmaccessmanager.cpp" line="140"/>
+ <source>Trying again...</source>
<translation type="unfinished"></translation>
</message>
</context>
@@ -7532,7 +8303,6 @@ Please open the &quot;Nexus&quot;-tab</source>
<message>
<location filename="tutorials/tutorial_firststeps_settings.js" line="19"/>
<source>Use this interface to obtain an API key from NexusMods. This is used for all API connections - downloads, updates etc. MO2 uses the Windows Credential Manager to store this data securely. If the SSO page on Nexus is failing, use the manual entry and copy the API key from your profile.</source>
- <oldsource>Use this interface to obtain an API key from NexusMods.This is used for all API connections - downloads, updatesetc. MO2 uses the Windows Credential Manager to storethis data securely. If the SSO page on Nexus is failing,use the manual entry and copy the API key from your profile.</oldsource>
<translation type="unfinished"></translation>
</message>
</context>
diff --git a/src/organizercore.cpp b/src/organizercore.cpp
index 81ec7f43..df505202 100644
--- a/src/organizercore.cpp
+++ b/src/organizercore.cpp
@@ -1,5 +1,4 @@
#include "organizercore.h"
-
#include "delayedfilewriter.h"
#include "guessedvalue.h"
#include "imodinterface.h"
@@ -14,7 +13,6 @@
#include "plugincontainer.h"
#include "pluginlistsortproxy.h"
#include "profile.h"
-#include "logbuffer.h"
#include "credentialsdialog.h"
#include "filedialogmemory.h"
#include "modinfodialog.h"
@@ -31,11 +29,11 @@
#include "appconfig.h"
#include <report.h>
#include <questionboxmemory.h>
-#include "lockeddialog.h"
#include "instancemanager.h"
#include <scriptextender.h>
-#include "helper.h"
#include "previewdialog.h"
+#include "env.h"
+#include "envmodule.h"
#include <QApplication>
#include <QCoreApplication>
@@ -76,108 +74,6 @@ using namespace MOBase;
//static
CrashDumpsType OrganizerCore::m_globalCrashDumpsType = CrashDumpsType::None;
-static bool isOnline()
-{
- QList<QNetworkInterface> interfaces = QNetworkInterface::allInterfaces();
-
- bool connected = false;
- for (auto iter = interfaces.begin(); iter != interfaces.end() && !connected;
- ++iter) {
- if ((iter->flags() & QNetworkInterface::IsUp)
- && (iter->flags() & QNetworkInterface::IsRunning)
- && !(iter->flags() & QNetworkInterface::IsLoopBack)) {
- auto addresses = iter->addressEntries();
- if (addresses.count() == 0) {
- continue;
- }
- qDebug("interface %s seems to be up (address: %s)",
- qUtf8Printable(iter->humanReadableName()),
- qUtf8Printable(addresses[0].ip().toString()));
- connected = true;
- }
- }
-
- return connected;
-}
-
-static bool renameFile(const QString &oldName, const QString &newName,
- bool overwrite = true)
-{
- if (overwrite && QFile::exists(newName)) {
- QFile::remove(newName);
- }
- return QFile::rename(oldName, newName);
-}
-
-static std::wstring getProcessName(HANDLE process)
-{
- wchar_t buffer[MAX_PATH];
- const wchar_t *fileName = L"unknown";
-
- if (process == nullptr) return fileName;
-
- if (::GetProcessImageFileNameW(process, buffer, MAX_PATH) != 0) {
- fileName = wcsrchr(buffer, L'\\');
- if (fileName == nullptr) {
- fileName = buffer;
- }
- else {
- fileName += 1;
- }
- }
-
- return fileName;
-}
-
-// Get parent PID for the given process, return 0 on failure
-static DWORD getProcessParentID(DWORD pid)
-{
- HANDLE th = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
- PROCESSENTRY32 pe = { 0 };
- pe.dwSize = sizeof(PROCESSENTRY32);
-
- DWORD res = 0;
- if (Process32First(th, &pe))
- do {
- if (pe.th32ProcessID == pid) {
- res = pe.th32ParentProcessID;
- break;
- }
- } while (Process32Next(th, &pe));
-
- CloseHandle(th);
-
- return res;
-}
-
-static void startSteam(QWidget *widget)
-{
- QSettings steamSettings("HKEY_CURRENT_USER\\Software\\Valve\\Steam",
- QSettings::NativeFormat);
- QString exe = steamSettings.value("SteamExe", "").toString();
- if (!exe.isEmpty()) {
- exe = QString("\"%1\"").arg(exe);
- // See if username and password supplied. If so, pass them into steam.
- QStringList args;
- QString username;
- QString password;
- if (Settings::instance().getSteamLogin(username, password)) {
- args << "-login";
- args << username;
- if (password != "") {
- args << password;
- }
- }
- if (!QProcess::startDetached(exe, args)) {
- reportError(QObject::tr("Failed to start \"%1\"").arg(exe));
- } else {
- QMessageBox::information(
- widget, QObject::tr("Waiting"),
- QObject::tr("Please press OK once you're logged into steam."));
- }
- }
-}
-
template <typename InputIterator>
QStringList toStringList(InputIterator current, InputIterator end)
{
@@ -188,93 +84,13 @@ QStringList toStringList(InputIterator current, InputIterator end)
return result;
}
-bool checkService()
-{
- SC_HANDLE serviceManagerHandle = NULL;
- SC_HANDLE serviceHandle = NULL;
- LPSERVICE_STATUS_PROCESS serviceStatus = NULL;
- LPQUERY_SERVICE_CONFIG serviceConfig = NULL;
- bool serviceRunning = true;
-
- DWORD bytesNeeded;
-
- try {
- serviceManagerHandle = OpenSCManager(NULL, NULL, SERVICE_QUERY_STATUS | SERVICE_QUERY_CONFIG);
- if (!serviceManagerHandle) {
- qWarning("failed to open service manager (query status) (error %d)", GetLastError());
- throw 1;
- }
-
- serviceHandle = OpenService(serviceManagerHandle, L"EventLog", SERVICE_QUERY_STATUS | SERVICE_QUERY_CONFIG);
- if (!serviceHandle) {
- qWarning("failed to open EventLog service (query status) (error %d)", GetLastError());
- throw 2;
- }
-
- if (QueryServiceConfig(serviceHandle, NULL, 0, &bytesNeeded)
- || (GetLastError() != ERROR_INSUFFICIENT_BUFFER)) {
- qWarning("failed to get size of service config (error %d)", GetLastError());
- throw 3;
- }
-
- DWORD serviceConfigSize = bytesNeeded;
- serviceConfig = (LPQUERY_SERVICE_CONFIG)LocalAlloc(LMEM_FIXED, serviceConfigSize);
- if (!QueryServiceConfig(serviceHandle, serviceConfig, serviceConfigSize, &bytesNeeded)) {
- qWarning("failed to query service config (error %d)", GetLastError());
- throw 4;
- }
-
- if (serviceConfig->dwStartType == SERVICE_DISABLED) {
- qCritical("Windows Event Log service is disabled!");
- serviceRunning = false;
- }
-
- if (QueryServiceStatusEx(serviceHandle, SC_STATUS_PROCESS_INFO, NULL, 0, &bytesNeeded)
- || (GetLastError() != ERROR_INSUFFICIENT_BUFFER)) {
- qWarning("failed to get size of service status (error %d)", GetLastError());
- throw 5;
- }
-
- DWORD serviceStatusSize = bytesNeeded;
- serviceStatus = (LPSERVICE_STATUS_PROCESS)LocalAlloc(LMEM_FIXED, serviceStatusSize);
- if (!QueryServiceStatusEx(serviceHandle, SC_STATUS_PROCESS_INFO, (LPBYTE)serviceStatus, serviceStatusSize, &bytesNeeded)) {
- qWarning("failed to query service status (error %d)", GetLastError());
- throw 6;
- }
-
- if (serviceStatus->dwCurrentState != SERVICE_RUNNING) {
- qCritical("Windows Event Log service is not running");
- serviceRunning = false;
- }
- }
- catch (int e) {
- UNUSED_VAR(e);
- serviceRunning = false;
- }
-
- if (serviceStatus) {
- LocalFree(serviceStatus);
- }
- if (serviceConfig) {
- LocalFree(serviceConfig);
- }
- if (serviceHandle) {
- CloseServiceHandle(serviceHandle);
- }
- if (serviceManagerHandle) {
- CloseServiceHandle(serviceManagerHandle);
- }
-
- return serviceRunning;
-}
-
-OrganizerCore::OrganizerCore(const QSettings &initSettings)
+OrganizerCore::OrganizerCore(Settings &settings)
: m_UserInterface(nullptr)
, m_PluginContainer(nullptr)
, m_GameName()
, m_CurrentProfile(nullptr)
- , m_Settings(initSettings)
+ , m_Settings(settings)
, m_Updater(NexusInterface::instance(m_PluginContainer))
, m_AboutToRun()
, m_FinishedRun()
@@ -290,15 +106,13 @@ OrganizerCore::OrganizerCore(const QSettings &initSettings)
, m_ArchivesInit(false)
, m_PluginListsWriter(std::bind(&OrganizerCore::savePluginList, this))
{
- m_DownloadManager.setOutputDirectory(m_Settings.getDownloadDirectory());
- m_DownloadManager.setPreferredServers(m_Settings.getPreferredServers());
-
- NexusInterface::instance(m_PluginContainer)->setCacheDirectory(m_Settings.getCacheDirectory());
+ m_DownloadManager.setOutputDirectory(m_Settings.paths().downloads());
- MOBase::QuestionBoxMemory::init(initSettings.fileName());
+ NexusInterface::instance(m_PluginContainer)->setCacheDirectory(
+ m_Settings.paths().cache());
- m_InstallationManager.setModsDirectory(m_Settings.getModDirectory());
- m_InstallationManager.setDownloadDirectory(m_Settings.getDownloadDirectory());
+ m_InstallationManager.setModsDirectory(m_Settings.paths().mods());
+ m_InstallationManager.setDownloadDirectory(m_Settings.paths().downloads());
connect(&m_DownloadManager, SIGNAL(downloadSpeed(QString, int)), this,
SLOT(downloadSpeed(QString, int)));
@@ -335,217 +149,119 @@ OrganizerCore::~OrganizerCore()
m_RefresherThread.exit();
m_RefresherThread.wait();
- prepareStart();
+ saveCurrentProfile();
// profile has to be cleaned up before the modinfo-buffer is cleared
delete m_CurrentProfile;
m_CurrentProfile = nullptr;
ModInfo::clear();
- LogBuffer::cleanQuit();
m_ModList.setProfile(nullptr);
// NexusInterface::instance()->cleanup();
delete m_DirectoryStructure;
}
-QString OrganizerCore::commitSettings(const QString &iniFile)
-{
- if (!shellRename(iniFile + ".new", iniFile, true, qApp->activeWindow())) {
- DWORD err = ::GetLastError();
- // make a second attempt using qt functions but if that fails print the
- // error from the first attempt
- if (!renameFile(iniFile + ".new", iniFile)) {
- return windowsErrorString(err);
- }
- }
- return QString();
-}
-
-QSettings::Status OrganizerCore::storeSettings(const QString &fileName)
+void OrganizerCore::storeSettings()
{
- QSettings settings(fileName, QSettings::IniFormat);
-
- if (m_UserInterface != nullptr) {
- m_UserInterface->storeSettings(settings);
- }
-
if (m_CurrentProfile != nullptr) {
- settings.setValue("selected_profile",
- m_CurrentProfile->name().toUtf8().constData());
+ m_Settings.game().setSelectedProfileName(m_CurrentProfile->name());
}
- m_ExecutablesList.store(settings);
+ m_ExecutablesList.store(m_Settings);
- FileDialogMemory::save(settings);
+ FileDialogMemory::save(m_Settings);
- settings.sync();
- return settings.status();
-}
+ const auto result = m_Settings.sync();
-void OrganizerCore::storeSettings()
-{
- QString iniFile = qApp->property("dataPath").toString() + "/"
- + QString::fromStdWString(AppConfig::iniFileName());
- if (QFileInfo(iniFile).exists()) {
- if (!shellCopy(iniFile, iniFile + ".new", true, qApp->activeWindow())) {
- QMessageBox::critical(
- qApp->activeWindow(), tr("Failed to write settings"),
- tr("An error occurred trying to update MO settings to %1: %2")
- .arg(iniFile, windowsErrorString(::GetLastError())));
- return;
- }
- }
-
- QString writeTarget = iniFile + ".new";
-
- QSettings::Status result = storeSettings(writeTarget);
-
- if (result == QSettings::NoError) {
- QString errMsg = commitSettings(iniFile);
- if (!errMsg.isEmpty()) {
- qWarning("settings file not writable, may be locked by another "
- "application, trying direct write");
- writeTarget = iniFile;
- result = storeSettings(iniFile);
- }
- }
if (result != QSettings::NoError) {
- QString reason = result == QSettings::AccessError
- ? tr("File is write protected")
- : result == QSettings::FormatError
- ? tr("Invalid file format (probably a bug)")
- : tr("Unknown error %1").arg(result);
- QMessageBox::critical(
- qApp->activeWindow(), tr("Failed to write settings"),
- tr("An error occurred trying to write back MO settings to %1: %2")
- .arg(writeTarget, reason));
- }
-}
-
-bool OrganizerCore::testForSteam(bool *found, bool *access)
-{
- HANDLE hProcessSnap;
- HANDLE hProcess;
- PROCESSENTRY32 pe32;
- DWORD lastError;
-
- if (found == nullptr || access == nullptr) {
- return false;
- }
-
- // Take a snapshot of all processes in the system.
- hProcessSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
- if (hProcessSnap == INVALID_HANDLE_VALUE) {
- lastError = GetLastError();
- qCritical("unable to get snapshot of processes (error %d)", lastError);
- return false;
- }
-
- // Retrieve information about the first process,
- // and exit if unsuccessful
- pe32.dwSize = sizeof(PROCESSENTRY32);
- if (!Process32First(hProcessSnap, &pe32)) {
- lastError = GetLastError();
- qCritical("unable to get first process (error %d)", lastError);
- CloseHandle(hProcessSnap);
- return false;
- }
-
- *found = false;
- *access = true;
-
- // Now walk the snapshot of processes, and
- // display information about each process in turn
- do {
- if ((_tcsicmp(pe32.szExeFile, L"Steam.exe") == 0) ||
- (_tcsicmp(pe32.szExeFile, L"SteamService.exe") == 0)) {
-
- *found = true;
+ QString reason;
- // Try to open the process to determine if MO has the proper access
- hProcess = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ,
- FALSE, pe32.th32ProcessID);
- if (hProcess == NULL) {
- lastError = GetLastError();
- if (lastError == ERROR_ACCESS_DENIED) {
- *access = false;
- }
- } else {
- CloseHandle(hProcess);
- }
- break;
+ if (result == QSettings::AccessError) {
+ reason = tr("File is write protected");
+ } else if (result == QSettings::FormatError) {
+ reason = tr("Invalid file format (probably a bug)");
+ } else {
+ reason = tr("Unknown error %1").arg(result);
}
-} while(Process32Next(hProcessSnap, &pe32));
-
-CloseHandle(hProcessSnap);
-return true;
-
+ QMessageBox::critical(
+ qApp->activeWindow(), tr("Failed to write settings"),
+ tr("An error occurred trying to write back MO settings to %1: %2")
+ .arg(m_Settings.filename(), reason));
+ }
}
-void OrganizerCore::updateExecutablesList(QSettings &settings)
+void OrganizerCore::updateExecutablesList()
{
if (m_PluginContainer == nullptr) {
- qCritical("can't update executables list now");
+ log::error("can't update executables list now");
return;
}
- m_ExecutablesList.load(managedGame(), settings);
+ m_ExecutablesList.load(managedGame(), m_Settings);
// TODO this has nothing to do with executables list move to an appropriate
// function!
- ModInfo::updateFromDisc(m_Settings.getModDirectory(), &m_DirectoryStructure,
- m_PluginContainer, m_Settings.displayForeign(), managedGame());
+ ModInfo::updateFromDisc(
+ m_Settings.paths().mods(), &m_DirectoryStructure,
+ m_PluginContainer, m_Settings.interface().displayForeign(), managedGame());
}
-void OrganizerCore::setUserInterface(IUserInterface *userInterface,
- QWidget *widget)
+void OrganizerCore::setUserInterface(IUserInterface* ui)
{
storeSettings();
- m_UserInterface = userInterface;
+ m_UserInterface = ui;
- if (widget != nullptr) {
- connect(&m_ModList, SIGNAL(modlistChanged(QModelIndex, int)), widget,
+ QWidget* w = nullptr;
+ if (m_UserInterface) {
+ w = m_UserInterface->qtWidget();
+ }
+
+ if (w) {
+ connect(&m_ModList, SIGNAL(modlistChanged(QModelIndex, int)), w,
SLOT(modlistChanged(QModelIndex, int)));
- connect(&m_ModList, SIGNAL(modlistChanged(QModelIndexList, int)), widget,
+ connect(&m_ModList, SIGNAL(modlistChanged(QModelIndexList, int)), w,
SLOT(modlistChanged(QModelIndexList, int)));
- connect(&m_ModList, SIGNAL(showMessage(QString)), widget,
+ connect(&m_ModList, SIGNAL(showMessage(QString)), w,
SLOT(showMessage(QString)));
- connect(&m_ModList, SIGNAL(modRenamed(QString, QString)), widget,
+ connect(&m_ModList, SIGNAL(modRenamed(QString, QString)), w,
SLOT(modRenamed(QString, QString)));
- connect(&m_ModList, SIGNAL(modUninstalled(QString)), widget,
+ connect(&m_ModList, SIGNAL(modUninstalled(QString)), w,
SLOT(modRemoved(QString)));
- connect(&m_ModList, SIGNAL(removeSelectedMods()), widget,
+ connect(&m_ModList, SIGNAL(removeSelectedMods()), w,
SLOT(removeMod_clicked()));
- connect(&m_ModList, SIGNAL(clearOverwrite()), widget,
+ connect(&m_ModList, SIGNAL(clearOverwrite()), w,
SLOT(clearOverwrite()));
- connect(&m_ModList, SIGNAL(requestColumnSelect(QPoint)), widget,
+ connect(&m_ModList, SIGNAL(requestColumnSelect(QPoint)), w,
SLOT(displayColumnSelection(QPoint)));
- connect(&m_ModList, SIGNAL(fileMoved(QString, QString, QString)), widget,
+ connect(&m_ModList, SIGNAL(fileMoved(QString, QString, QString)), w,
SLOT(fileMoved(QString, QString, QString)));
- connect(&m_ModList, SIGNAL(modorder_changed()), widget,
+ connect(&m_ModList, SIGNAL(modorder_changed()), w,
SLOT(modorder_changed()));
- connect(&m_PluginList, SIGNAL(writePluginsList()), widget,
+ connect(&m_PluginList, SIGNAL(writePluginsList()), w,
SLOT(esplist_changed()));
- connect(&m_PluginList, SIGNAL(esplist_changed()), widget,
+ connect(&m_PluginList, SIGNAL(esplist_changed()), w,
SLOT(esplist_changed()));
- connect(&m_DownloadManager, SIGNAL(showMessage(QString)), widget,
+ connect(&m_DownloadManager, SIGNAL(showMessage(QString)), w,
SLOT(showMessage(QString)));
}
- m_InstallationManager.setParentWidget(widget);
- m_Updater.setUserInterface(widget);
+ m_InstallationManager.setParentWidget(w);
+ m_Updater.setUserInterface(w);
+ m_UILocker.setUserInterface(w);
+ m_DownloadManager.setParentWidget(w);
- if (userInterface != nullptr) {
- // this currently wouldn't work reliably if the ui isn't initialized yet to
- // display the result
- if (isOnline() && !m_Settings.offlineMode()) {
- m_Updater.testForUpdate();
- } else {
- qDebug("user doesn't seem to be connected to the internet");
- }
+ checkForUpdates();
+}
+
+void OrganizerCore::checkForUpdates()
+{
+ // this currently wouldn't work reliably if the ui isn't initialized yet to
+ // display the result
+ if (m_UserInterface != nullptr) {
+ m_Updater.testForUpdate(m_Settings);
}
}
@@ -575,7 +291,7 @@ void OrganizerCore::disconnectPlugins()
m_DownloadManager.setPluginContainer(nullptr);
m_ModList.setPluginContainer(nullptr);
- m_Settings.clearPlugins();
+ m_Settings.plugins().clearPlugins();
m_GamePlugin = nullptr;
m_PluginContainer = nullptr;
}
@@ -604,9 +320,9 @@ bool OrganizerCore::nexusApi(bool retry)
return false;
} else {
QString apiKey;
- if (m_Settings.getNexusApiKey(apiKey)) {
+ if (m_Settings.nexus().apiKey(apiKey)) {
// credentials stored or user entered them manually
- qDebug("attempt to verify nexus api key");
+ log::debug("attempt to verify nexus api key");
accessManager->apiCheck(apiKey);
return true;
} else {
@@ -628,7 +344,7 @@ void OrganizerCore::startMOUpdate()
void OrganizerCore::downloadRequestedNXM(const QString &url)
{
- qDebug("download requested: %s", qUtf8Printable(url));
+ log::debug("download requested: {}", url);
if (nexusApi()) {
m_PendingDownloads.append(url);
} else {
@@ -639,8 +355,12 @@ void OrganizerCore::downloadRequestedNXM(const QString &url)
void OrganizerCore::externalMessage(const QString &message)
{
if (MOShortcut moshortcut{ message } ) {
- if(moshortcut.hasExecutable())
- runShortcut(moshortcut);
+ if(moshortcut.hasExecutable()) {
+ processRunner()
+ .setFromShortcut(moshortcut)
+ .setWaitForCompletion(ProcessRunner::Refresh)
+ .run();
+ }
}
else if (isNxmLink(message)) {
MessageDialog::showMessage(tr("Download started"), qApp->activeWindow());
@@ -658,7 +378,7 @@ void OrganizerCore::downloadRequested(QNetworkReply *reply, QString gameName, in
}
} catch (const std::exception &e) {
MessageDialog::showMessage(tr("Download failed"), qApp->activeWindow());
- qCritical("exception starting download: %s", e.what());
+ log::error("exception starting download: {}", e.what());
}
}
@@ -671,7 +391,7 @@ void OrganizerCore::removeOrigin(const QString &name)
void OrganizerCore::downloadSpeed(const QString &serverName, int bytesPerSecond)
{
- m_Settings.setDownloadSpeed(serverName, bytesPerSecond);
+ m_Settings.network().setDownloadSpeed(serverName, bytesPerSecond);
}
InstallationManager *OrganizerCore::installationManager()
@@ -692,31 +412,35 @@ bool OrganizerCore::createDirectory(const QString &path) {
}
bool OrganizerCore::checkPathSymlinks() {
- bool hasSymlink = (QFileInfo(m_Settings.getProfileDirectory()).isSymLink() ||
- QFileInfo(m_Settings.getModDirectory()).isSymLink() ||
- QFileInfo(m_Settings.getOverwriteDirectory()).isSymLink());
+ const bool hasSymlink = (
+ QFileInfo(m_Settings.paths().profiles()).isSymLink() ||
+ QFileInfo(m_Settings.paths().mods()).isSymLink() ||
+ QFileInfo(m_Settings.paths().overwrite()).isSymLink());
+
if (hasSymlink) {
- QMessageBox::critical(nullptr, QObject::tr("Error"),
- QObject::tr("One of the configured MO2 directories (profiles, mods, or overwrite) "
- "is on a path containing a symbolic (or other) link. This is incompatible "
- "with MO2's VFS system."));
+ log::warn("{}", QObject::tr(
+ "One of the configured MO2 directories (profiles, mods, or overwrite) "
+ "is on a path containing a symbolic (or other) link. This is likely to "
+ "be incompatible with MO2's virtual filesystem."));
+
return false;
}
+
return true;
}
bool OrganizerCore::bootstrap() {
- return createDirectory(m_Settings.getProfileDirectory()) &&
- createDirectory(m_Settings.getModDirectory()) &&
- createDirectory(m_Settings.getDownloadDirectory()) &&
- createDirectory(m_Settings.getOverwriteDirectory()) &&
+ return createDirectory(m_Settings.paths().profiles()) &&
+ createDirectory(m_Settings.paths().mods()) &&
+ createDirectory(m_Settings.paths().downloads()) &&
+ createDirectory(m_Settings.paths().overwrite()) &&
createDirectory(QString::fromStdWString(crashDumpsPath())) &&
checkPathSymlinks() && cycleDiagnostics();
}
void OrganizerCore::createDefaultProfile()
{
- QString profilesPath = settings().getProfileDirectory();
+ QString profilesPath = settings().paths().profiles();
if (QDir(profilesPath).entryList(QDir::AllDirs | QDir::NoDotAndDotDot).size()
== 0) {
Profile newProf("Default", managedGame(), false);
@@ -728,23 +452,47 @@ void OrganizerCore::prepareVFS()
m_USVFS.updateMapping(fileMapping(m_CurrentProfile->name(), QString()));
}
-void OrganizerCore::updateVFSParams(int logLevel, int crashDumpsType, QString executableBlacklist) {
+void OrganizerCore::updateVFSParams(
+ log::Levels logLevel, CrashDumpsType crashDumpsType,
+ const QString& crashDumpsPath,
+ std::chrono::seconds spawnDelay, QString executableBlacklist)
+{
setGlobalCrashDumpsType(crashDumpsType);
- m_USVFS.updateParams(logLevel, crashDumpsType, executableBlacklist);
+ m_USVFS.updateParams(
+ logLevel, crashDumpsType, crashDumpsPath, spawnDelay, executableBlacklist);
}
-bool OrganizerCore::cycleDiagnostics() {
- if (int maxDumps = settings().crashDumpsMax())
- removeOldFiles(QString::fromStdWString(crashDumpsPath()), "*.dmp", maxDumps, QDir::Time|QDir::Reversed);
+void OrganizerCore::setLogLevel(log::Levels level)
+{
+ m_Settings.diagnostics().setLogLevel(level);
+
+ updateVFSParams(
+ m_Settings.diagnostics().logLevel(),
+ m_Settings.diagnostics().crashDumpsType(),
+ QString::fromStdWString(crashDumpsPath()),
+ m_Settings.diagnostics().spawnDelay(),
+ m_Settings.executablesBlacklist());
+
+ log::getDefault().setLevel(m_Settings.diagnostics().logLevel());
+}
+
+bool OrganizerCore::cycleDiagnostics()
+{
+ const auto maxDumps = settings().diagnostics().crashDumpsMax();
+ const auto path = QString::fromStdWString(crashDumpsPath());
+
+ if (maxDumps > 0) {
+ removeOldFiles(path, "*.dmp", maxDumps, QDir::Time|QDir::Reversed);
+ }
+
return true;
}
-//static
-void OrganizerCore::setGlobalCrashDumpsType(int crashDumpsType) {
- m_globalCrashDumpsType = ::crashDumpsType(crashDumpsType);
+void OrganizerCore::setGlobalCrashDumpsType(CrashDumpsType type)
+{
+ m_globalCrashDumpsType = type;
}
-//static
std::wstring OrganizerCore::crashDumpsPath() {
return (
qApp->property("dataPath").toString() + "/"
@@ -769,7 +517,7 @@ void OrganizerCore::setCurrentProfile(const QString &profileName)
return;
}
- QDir profileBaseDir(settings().getProfileDirectory());
+ QDir profileBaseDir(settings().paths().profiles());
QString profileDir = profileBaseDir.absoluteFilePath(profileName);
if (!QDir(profileDir).exists()) {
@@ -793,16 +541,11 @@ void OrganizerCore::setCurrentProfile(const QString &profileName)
m_CurrentProfile->deactivateInvalidation();
}
+ m_Settings.game().setSelectedProfileName(m_CurrentProfile->name());
+
connect(m_CurrentProfile, SIGNAL(modStatusChanged(uint)), this, SLOT(modStatusChanged(uint)));
connect(m_CurrentProfile, SIGNAL(modStatusChanged(QList<uint>)), this, SLOT(modStatusChanged(QList<uint>)));
refreshDirectoryStructure();
-
- //This line is not actually needed and was only added to allow some
- //outside detection of Mo2 profile change. (like BaobobMiller utility)
- if (m_CurrentProfile != nullptr) {
- settings().directInterface().setValue("selected_profile",
- m_CurrentProfile->name().toUtf8().constData());
- }
}
MOBase::IModRepositoryBridge *OrganizerCore::createNexusBridge() const
@@ -830,22 +573,22 @@ QString OrganizerCore::profilePath() const
QString OrganizerCore::downloadsPath() const
{
- return QDir::fromNativeSeparators(m_Settings.getDownloadDirectory());
+ return QDir::fromNativeSeparators(m_Settings.paths().downloads());
}
QString OrganizerCore::overwritePath() const
{
- return QDir::fromNativeSeparators(m_Settings.getOverwriteDirectory());
+ return QDir::fromNativeSeparators(m_Settings.paths().overwrite());
}
QString OrganizerCore::basePath() const
{
- return QDir::fromNativeSeparators(m_Settings.getBaseDirectory());
+ return QDir::fromNativeSeparators(m_Settings.paths().base());
}
QString OrganizerCore::modsPath() const
{
- return QDir::fromNativeSeparators(m_Settings.getModDirectory());
+ return QDir::fromNativeSeparators(m_Settings.paths().mods());
}
MOBase::VersionInfo OrganizerCore::appVersion() const
@@ -875,10 +618,10 @@ MOBase::IModInterface *OrganizerCore::createMod(GuessedValue<QString> &name)
return nullptr;
}
- m_InstallationManager.setModsDirectory(m_Settings.getModDirectory());
+ m_InstallationManager.setModsDirectory(m_Settings.paths().mods());
QString targetDirectory
- = QDir::fromNativeSeparators(m_Settings.getModDirectory())
+ = QDir::fromNativeSeparators(m_Settings.paths().mods())
.append("/")
.append(name);
@@ -918,26 +661,26 @@ void OrganizerCore::modDataChanged(MOBase::IModInterface *)
QVariant OrganizerCore::pluginSetting(const QString &pluginName,
const QString &key) const
{
- return m_Settings.pluginSetting(pluginName, key);
+ return m_Settings.plugins().setting(pluginName, key);
}
void OrganizerCore::setPluginSetting(const QString &pluginName,
const QString &key, const QVariant &value)
{
- m_Settings.setPluginSetting(pluginName, key, value);
+ m_Settings.plugins().setSetting(pluginName, key, value);
}
QVariant OrganizerCore::persistent(const QString &pluginName,
const QString &key,
const QVariant &def) const
{
- return m_Settings.pluginPersistent(pluginName, key, def);
+ return m_Settings.plugins().persistent(pluginName, key, def);
}
void OrganizerCore::setPersistent(const QString &pluginName, const QString &key,
const QVariant &value, bool sync)
{
- m_Settings.setPluginPersistent(pluginName, key, value, sync);
+ m_Settings.plugins().setPersistent(pluginName, key, value, sync);
}
QString OrganizerCore::pluginDataPath() const
@@ -966,8 +709,8 @@ MOBase::IModInterface *OrganizerCore::installMod(const QString &fileName,
modName.update(initModName, GUESS_USER);
}
m_CurrentProfile->writeModlistNow();
- m_InstallationManager.setModsDirectory(m_Settings.getModDirectory());
- if (m_InstallationManager.install(fileName, modName, hasIniTweaks)) {
+ m_InstallationManager.setModsDirectory(m_Settings.paths().mods());
+ if (m_InstallationManager.install(fileName, modName, hasIniTweaks) == IPluginInstaller::RESULT_SUCCESS) {
MessageDialog::showMessage(tr("Installation successful"),
qApp->activeWindow());
refreshModList();
@@ -992,9 +735,12 @@ MOBase::IModInterface *OrganizerCore::installMod(const QString &fileName,
reportError(tr("mod not found: %1").arg(qUtf8Printable(modName)));
}
} else if (m_InstallationManager.wasCancelled()) {
- QMessageBox::information(qApp->activeWindow(), tr("Installation cancelled"),
- tr("The mod was not installed completely."),
+ QMessageBox::information(qApp->activeWindow(), tr("Extraction cancelled"),
+ tr("The installation was cancelled while extracting files. "
+ "If this was prior to a FOMOD setup, this warning may be ignored. "
+ "However, if this was during installation, the mod will likely be missing files."),
QMessageBox::Ok);
+ refreshModList();
}
return nullptr;
}
@@ -1031,8 +777,8 @@ void OrganizerCore::installDownload(int index)
m_CurrentProfile->writeModlistNow();
bool hasIniTweaks = false;
- m_InstallationManager.setModsDirectory(m_Settings.getModDirectory());
- if (m_InstallationManager.install(fileName, modName, hasIniTweaks)) {
+ m_InstallationManager.setModsDirectory(m_Settings.paths().mods());
+ if (m_InstallationManager.install(fileName, modName, hasIniTweaks) == IPluginInstaller::RESULT_SUCCESS) {
MessageDialog::showMessage(tr("Installation successful"),
qApp->activeWindow());
refreshModList();
@@ -1060,9 +806,12 @@ void OrganizerCore::installDownload(int index)
emit modInstalled(modName);
} else if (m_InstallationManager.wasCancelled()) {
- QMessageBox::information(
- qApp->activeWindow(), tr("Installation cancelled"),
- tr("The mod was not installed completely."), QMessageBox::Ok);
+ QMessageBox::information(qApp->activeWindow(), tr("Extraction cancelled"),
+ tr("The installation was cancelled while extracting files. "
+ "If this was prior to a FOMOD setup, this warning may be ignored. "
+ "However, if this was during installation, the mod will likely be missing files."),
+ QMessageBox::Ok);
+ refreshModList();
}
} catch (const std::exception &e) {
reportError(e.what());
@@ -1197,108 +946,6 @@ QStringList OrganizerCore::modsSortedByProfilePriority() const
return res;
}
-QString OrganizerCore::findJavaInstallation(const QString& jarFile)
-{
- if (!jarFile.isEmpty()) {
- // try to find java automatically based on the given jar file
- std::wstring jarFileW = jarFile.toStdWString();
-
- WCHAR buffer[MAX_PATH];
- if (::FindExecutableW(jarFileW.c_str(), nullptr, buffer) > (HINSTANCE)32) {
- DWORD binaryType = 0UL;
- if (!::GetBinaryTypeW(buffer, &binaryType)) {
- qDebug("failed to determine binary type of \"%ls\": %lu", buffer, ::GetLastError());
- } else if (binaryType == SCS_32BIT_BINARY || binaryType == SCS_64BIT_BINARY) {
- return QString::fromWCharArray(buffer);
- }
- }
- }
-
- // second attempt: look to the registry
- QSettings reg("HKEY_LOCAL_MACHINE\\Software\\JavaSoft\\Java Runtime Environment", QSettings::NativeFormat);
- if (reg.contains("CurrentVersion")) {
- QString currentVersion = reg.value("CurrentVersion").toString();
- return reg.value(QString("%1/JavaHome").arg(currentVersion)).toString().append("\\bin\\javaw.exe");
- }
-
- // not found
- return {};
-}
-
-bool OrganizerCore::getFileExecutionContext(
- QWidget* parent, const QFileInfo &targetInfo,
- QFileInfo &binaryInfo, QString &arguments, FileExecutionTypes& type)
-{
- QString extension = targetInfo.suffix();
- if ((extension.compare("cmd", Qt::CaseInsensitive) == 0) ||
- (extension.compare("com", Qt::CaseInsensitive) == 0) ||
- (extension.compare("bat", Qt::CaseInsensitive) == 0)) {
- binaryInfo = QFileInfo("C:\\Windows\\System32\\cmd.exe");
- arguments = QString("/C \"%1\"").arg(QDir::toNativeSeparators(targetInfo.absoluteFilePath()));
- type = FileExecutionTypes::Executable;
- return true;
- } else if (extension.compare("exe", Qt::CaseInsensitive) == 0) {
- binaryInfo = targetInfo;
- type = FileExecutionTypes::Executable;
- return true;
- } else if (extension.compare("jar", Qt::CaseInsensitive) == 0) {
- auto java = findJavaInstallation(targetInfo.absoluteFilePath());
-
- if (java.isEmpty()) {
- java = QFileDialog::getOpenFileName(
- parent, QObject::tr("Select binary"),
- QString(), QObject::tr("Binary") + " (*.exe)");
- }
-
- if (java.isEmpty()) {
- return false;
- }
-
- binaryInfo = QFileInfo(java);
- arguments = QString("-jar \"%1\"").arg(QDir::toNativeSeparators(targetInfo.absoluteFilePath()));
- type = FileExecutionTypes::Executable;
-
- return true;
- } else {
- type = FileExecutionTypes::Other;
- return true;
- }
-}
-
-bool OrganizerCore::executeFileVirtualized(
- QWidget* parent, const QFileInfo& targetInfo)
-{
- QFileInfo binaryInfo;
- QString arguments;
- FileExecutionTypes type;
-
- if (!getFileExecutionContext(parent, targetInfo, binaryInfo, arguments, type)) {
- return false;
- }
-
- switch (type)
- {
- case FileExecutionTypes::Executable: {
- spawnBinaryDirect(
- binaryInfo, arguments, currentProfile()->name(),
- targetInfo.absolutePath(), "", "");
-
- return true;
- }
-
- case FileExecutionTypes::Other: {
- ::ShellExecuteW(nullptr, L"open",
- ToWString(targetInfo.absoluteFilePath()).c_str(),
- nullptr, nullptr, SW_SHOWNORMAL);
-
- return true;
- }
- }
-
- // nop
- return false;
-}
-
bool OrganizerCore::previewFileWithAlternatives(
QWidget* parent, QString fileName, int selectedOrigin)
{
@@ -1322,7 +969,7 @@ bool OrganizerCore::previewFileWithAlternatives(
else {
// crude: we search for the next slash after the base mod directory to skip
// everything up to the data-relative directory
- int offset = settings().getModDirectory().size() + 1;
+ int offset = settings().paths().mods().size() + 1;
offset = fileName.indexOf("/", offset);
fileName = fileName.mid(offset + 1);
}
@@ -1337,7 +984,8 @@ bool OrganizerCore::previewFileWithAlternatives(
}
// set up preview dialog
- PreviewDialog preview(fileName);
+ PreviewDialog preview(fileName, parent);
+
auto addFunc = [&](int originId) {
FilesOrigin &origin = directoryStructure()->getOriginByID(originId);
QString filePath = QDir::fromNativeSeparators(ToQString(origin.getPath())) + "/" + fileName;
@@ -1381,9 +1029,9 @@ bool OrganizerCore::previewFileWithAlternatives(
// sanity check, this shouldn't happen unless the caller passed an
// incorrect id
- qWarning().nospace()
- << "selected preview origin " << selectedOrigin << " not found in "
- << "list of alternatives";
+ log::warn(
+ "selected preview origin {} not found in list of alternatives",
+ selectedOrigin);
}
for (int id : origins) {
@@ -1392,16 +1040,7 @@ bool OrganizerCore::previewFileWithAlternatives(
}
if (preview.numVariants() > 0) {
- QSettings &s = settings().directInterface();
- QString key = QString("geometry/%1").arg(preview.objectName());
- if (s.contains(key)) {
- preview.restoreGeometry(s.value(key).toByteArray());
- }
-
preview.exec();
-
- s.setValue(key, preview.saveGeometry());
-
return true;
}
else {
@@ -1421,7 +1060,7 @@ bool OrganizerCore::previewFile(
return false;
}
- PreviewDialog preview(path);
+ PreviewDialog preview(path, parent);
QWidget *wid = m_PluginContainer->previewGenerator().genPreview(path);
if (wid == nullptr) {
@@ -1430,578 +1069,11 @@ bool OrganizerCore::previewFile(
}
preview.addVariant(originName, wid);
-
- QSettings &s = settings().directInterface();
- QString key = QString("geometry/%1").arg(preview.objectName());
- if (s.contains(key)) {
- preview.restoreGeometry(s.value(key).toByteArray());
- }
-
preview.exec();
- s.setValue(key, preview.saveGeometry());
-
return true;
}
-void OrganizerCore::spawnBinary(const QFileInfo &binary,
- const QString &arguments,
- const QDir &currentDirectory,
- const QString &steamAppID,
- const QString &customOverwrite,
- const QList<MOBase::ExecutableForcedLoadSetting> &forcedLibraries)
-{
- DWORD processExitCode = 0;
- HANDLE processHandle = spawnBinaryDirect(binary, arguments, m_CurrentProfile->name(), currentDirectory, steamAppID, customOverwrite, forcedLibraries, &processExitCode);
- if (processHandle != INVALID_HANDLE_VALUE) {
- refreshDirectoryStructure();
- // need to remove our stored load order because it may be outdated if a foreign tool changed the
- // file time. After removing that file, refreshESPList will use the file time as the order
- if (managedGame()->loadOrderMechanism() == IPluginGame::LoadOrderMechanism::FileTime) {
- qDebug("removing loadorder.txt");
- QFile::remove(m_CurrentProfile->getLoadOrderFileName());
- }
- refreshDirectoryStructure();
-
- refreshESPList(true);
- savePluginList();
-
- //These callbacks should not fiddle with directoy structure and ESPs.
- m_FinishedRun(binary.absoluteFilePath(), processExitCode);
- }
-}
-
-HANDLE OrganizerCore::spawnBinaryDirect(const QFileInfo &binary,
- const QString &arguments,
- const QString &profileName,
- const QDir &currentDirectory,
- const QString &steamAppID,
- const QString &customOverwrite,
- const QList<MOBase::ExecutableForcedLoadSetting> &forcedLibraries,
- LPDWORD exitCode)
-{
- HANDLE processHandle = spawnBinaryProcess(binary, arguments, profileName, currentDirectory, steamAppID, customOverwrite, forcedLibraries);
- if (Settings::instance().lockGUI() && processHandle != INVALID_HANDLE_VALUE) {
- std::unique_ptr<LockedDialog> dlg;
- ILockedWaitingForProcess* uilock = nullptr;
-
- if (m_UserInterface != nullptr) {
- uilock = m_UserInterface->lock();
- }
- else {
- // i.e. when running command line shortcuts there is no m_UserInterface
- dlg.reset(new LockedDialog);
- dlg->show();
- dlg->setEnabled(true);
- uilock = dlg.get();
- }
-
- ON_BLOCK_EXIT([&]() {
- if (m_UserInterface != nullptr) {
- m_UserInterface->unlock();
- } });
-
- DWORD ignoreExitCode;
- waitForProcessCompletion(processHandle, exitCode ? exitCode : &ignoreExitCode, uilock);
- cycleDiagnostics();
- }
-
- return processHandle;
-}
-
-
-HANDLE OrganizerCore::spawnBinaryProcess(const QFileInfo &binary,
- const QString &arguments,
- const QString &profileName,
- const QDir &currentDirectory,
- const QString &steamAppID,
- const QString &customOverwrite,
- const QList<MOBase::ExecutableForcedLoadSetting> &forcedLibraries)
-{
- prepareStart();
-
- if (!binary.exists()) {
- reportError(
- tr("Executable not found: %1").arg(qUtf8Printable(binary.absoluteFilePath())));
- return INVALID_HANDLE_VALUE;
- }
-
- if (!steamAppID.isEmpty()) {
- ::SetEnvironmentVariableW(L"SteamAPPId", ToWString(steamAppID).c_str());
- } else {
- ::SetEnvironmentVariableW(L"SteamAPPId",
- ToWString(m_Settings.getSteamAppID()).c_str());
- }
-
- QWidget *window = qApp->activeWindow();
- if ((window != nullptr) && (!window->isVisible())) {
- window = nullptr;
- }
-
- // This could possibly be extracted somewhere else but it's probably for when
- // we have more than one provider of game registration.
- if ((QFileInfo(
- managedGame()->gameDirectory().absoluteFilePath("steam_api.dll"))
- .exists()
- || QFileInfo(managedGame()->gameDirectory().absoluteFilePath(
- "steam_api64.dll"))
- .exists())
- && (m_Settings.getLoadMechanism() == LoadMechanism::LOAD_MODORGANIZER)) {
-
- bool steamFound = true;
- bool steamAccess = true;
- if (!testForSteam(&steamFound, &steamAccess)) {
- qCritical("unable to determine state of Steam");
- }
-
- if (!steamFound) {
- QDialogButtonBox::StandardButton result;
- result = QuestionBoxMemory::query(window, "steamQuery", binary.fileName(),
- tr("Start Steam?"),
- tr("Steam is required to be running already to correctly start the game. "
- "Should MO try to start steam now?"),
- QDialogButtonBox::Yes | QDialogButtonBox::No | QDialogButtonBox::Cancel);
- if (result == QDialogButtonBox::Yes) {
- startSteam(window);
-
- // double-check that Steam is started and MO has access
- steamFound = true;
- steamAccess = true;
- if (!testForSteam(&steamFound, &steamAccess)) {
- qCritical("unable to determine state of Steam");
- } else if (!steamFound) {
- qCritical("could not find Steam");
- }
-
- } else if (result == QDialogButtonBox::Cancel) {
- return INVALID_HANDLE_VALUE;
- }
- }
-
- if (!steamAccess) {
- QDialogButtonBox::StandardButton result;
- result = QuestionBoxMemory::query(window, "steamAdminQuery", binary.fileName(),
- tr("Steam: Access Denied"),
- tr("MO was denied access to the Steam process. This normally indicates that "
- "Steam is being run as administrator while MO is not. This can cause issues "
- "launching the game. It is recommended to not run Steam as administrator unless "
- "absolutely necessary.\n\n"
- "Restart MO as administrator?"),
- QDialogButtonBox::Yes | QDialogButtonBox::No | QDialogButtonBox::Cancel);
- if (result == QDialogButtonBox::Yes) {
- WCHAR cwd[MAX_PATH];
- if (!GetCurrentDirectory(MAX_PATH, cwd)) {
- qCritical("unable to get current directory (error %d)", GetLastError());
- cwd[0] = L'\0';
- }
- if (!Helper::adminLaunch(
- qApp->applicationDirPath().toStdWString(),
- qApp->applicationFilePath().toStdWString(),
- std::wstring(cwd))) {
- qCritical("unable to relaunch MO as admin");
- return INVALID_HANDLE_VALUE;
- }
- qApp->exit(0);
- return INVALID_HANDLE_VALUE;
- } else if (result == QDialogButtonBox::Cancel) {
- return INVALID_HANDLE_VALUE;
- }
- }
- }
-
- while (m_DirectoryUpdate) {
- ::Sleep(100);
- QCoreApplication::processEvents();
- }
-
- // need to make sure all data is saved before we start the application
- if (m_CurrentProfile != nullptr) {
- m_CurrentProfile->writeModlistNow(true);
- }
-
- // TODO: should also pass arguments
- if (m_AboutToRun(binary.absoluteFilePath())) {
- try {
- m_USVFS.updateMapping(fileMapping(profileName, customOverwrite));
- m_USVFS.updateForcedLibraries(forcedLibraries);
-
- } catch (const UsvfsConnectorException &e) {
- qDebug(e.what());
- return INVALID_HANDLE_VALUE;
- } catch (const std::exception &e) {
- QMessageBox::warning(window, tr("Error"), e.what());
- return INVALID_HANDLE_VALUE;
- }
-
- // Check if the Windows Event Logging service is running. For some reason, this seems to be
- // critical to the successful running of usvfs.
- if (!checkService()) {
- if (QuestionBoxMemory::query(window, QString("eventLogService"), binary.fileName(),
- tr("Windows Event Log Error"),
- tr("The Windows Event Log service is disabled and/or not running. This prevents"
- " USVFS from running properly. Your mods may not be working in the executable"
- " that you are launching. Note that you may have to restart MO and/or your PC"
- " after the service is fixed.\n\nContinue launching %1?").arg(binary.fileName()),
- QDialogButtonBox::Yes | QDialogButtonBox::No) == QDialogButtonBox::No) {
- return INVALID_HANDLE_VALUE;
- }
- }
-
- for (auto exec : settings().executablesBlacklist().split(";")) {
- if (exec.compare(binary.fileName(), Qt::CaseInsensitive) == 0) {
- if (QuestionBoxMemory::query(window, QString("blacklistedExecutable"), binary.fileName(),
- tr("Blacklisted Executable"),
- tr("The executable you are attempted to launch is blacklisted in the virtual file"
- " system. This will likely prevent the executable, and any executables that are"
- " launched by this one, from seeing any mods. This could extend to INI files, save"
- " games and any other virtualized files.\n\nContinue launching %1?").arg(binary.fileName()),
- QDialogButtonBox::Yes | QDialogButtonBox::No) == QDialogButtonBox::No) {
- return INVALID_HANDLE_VALUE;
- }
- }
- }
-
- QString modsPath = settings().getModDirectory();
-
- // Check if this a request with either an executable or a working directory under our mods folder
- // then will start the process in a virtualized "environment" with the appropriate paths fixed:
- // (i.e. mods\FNIS\path\exe => game\data\path\exe)
- QString cwdPath = currentDirectory.absolutePath();
- bool virtualizedCwd = cwdPath.startsWith(modsPath, Qt::CaseInsensitive);
- QString binPath = binary.absoluteFilePath();
- bool virtualizedBin = binPath.startsWith(modsPath, Qt::CaseInsensitive);
- if (virtualizedCwd || virtualizedBin) {
- if (virtualizedCwd) {
- int cwdOffset = cwdPath.indexOf('/', modsPath.length() + 1);
- QString adjustedCwd = cwdPath.mid(cwdOffset, -1);
- cwdPath = m_GamePlugin->dataDirectory().absolutePath();
- if (cwdOffset >= 0)
- cwdPath += adjustedCwd;
-
- }
-
- if (virtualizedBin) {
- int binOffset = binPath.indexOf('/', modsPath.length() + 1);
- QString adjustedBin = binPath.mid(binOffset, -1);
- binPath = m_GamePlugin->dataDirectory().absolutePath();
- if (binOffset >= 0)
- binPath += adjustedBin;
- }
-
- QString cmdline
- = QString("launch \"%1\" \"%2\" %3")
- .arg(QDir::toNativeSeparators(cwdPath),
- QDir::toNativeSeparators(binPath), arguments);
-
- qDebug() << "Spawning proxyed process <" << cmdline << ">";
-
- return startBinary(QFileInfo(QCoreApplication::applicationFilePath()),
- cmdline, QCoreApplication::applicationDirPath(), true);
- } else {
- qDebug() << "Spawning direct process <" << binPath << "," << arguments << "," << cwdPath << ">";
- return startBinary(binary, arguments, currentDirectory, true);
- }
- } else {
- qDebug("start of \"%s\" canceled by plugin",
- qUtf8Printable(binary.absoluteFilePath()));
- return INVALID_HANDLE_VALUE;
- }
-}
-
-HANDLE OrganizerCore::runShortcut(const MOShortcut& shortcut)
-{
- if (shortcut.hasInstance() && shortcut.instance() != InstanceManager::instance().currentInstance())
- throw std::runtime_error(
- QString("Refusing to run executable from different instance %1:%2")
- .arg(shortcut.instance(),shortcut.executable())
- .toLocal8Bit().constData());
-
- const Executable& exe = m_ExecutablesList.get(shortcut.executable());
-
- auto forcedLibaries = m_CurrentProfile->determineForcedLibraries(shortcut.executable());
- if (!m_CurrentProfile->forcedLibrariesEnabled(shortcut.executable())) {
- forcedLibaries.clear();
- }
-
- return spawnBinaryDirect(
- exe.binaryInfo(), exe.arguments(),
- m_CurrentProfile->name(),
- exe.workingDirectory().length() != 0
- ? exe.workingDirectory()
- : exe.binaryInfo().absolutePath(),
- exe.steamAppID(),
- "",
- forcedLibaries);
-}
-
-HANDLE OrganizerCore::startApplication(const QString &executable,
- const QStringList &args,
- const QString &cwd,
- const QString &profile,
- const QString &forcedCustomOverwrite,
- bool ignoreCustomOverwrite)
-{
- QFileInfo binary;
- QString arguments = args.join(" ");
- QString currentDirectory = cwd;
- QString profileName = profile;
- if (profile.length() == 0) {
- if (m_CurrentProfile != nullptr) {
- profileName = m_CurrentProfile->name();
- } else {
- throw MyException(tr("No profile set"));
- }
- }
- QString steamAppID;
- QString customOverwrite;
- QList<ExecutableForcedLoadSetting> forcedLibraries;
- if (executable.contains('\\') || executable.contains('/')) {
- // file path
-
- binary = QFileInfo(executable);
- if (binary.isRelative()) {
- // relative path, should be relative to game directory
- binary = QFileInfo(
- managedGame()->gameDirectory().absoluteFilePath(executable));
- }
- if (cwd.length() == 0) {
- currentDirectory = binary.absolutePath();
- }
- try {
- const Executable &exe = m_ExecutablesList.getByBinary(binary);
- steamAppID = exe.steamAppID();
- customOverwrite
- = m_CurrentProfile->setting("custom_overwrites", exe.title())
- .toString();
- if (m_CurrentProfile->forcedLibrariesEnabled(exe.title())) {
- forcedLibraries = m_CurrentProfile->determineForcedLibraries(exe.title());
- }
- } catch (const std::runtime_error &) {
- // nop
- }
- } else {
- // only a file name, search executables list
- try {
- const Executable &exe = m_ExecutablesList.get(executable);
- steamAppID = exe.steamAppID();
- customOverwrite
- = m_CurrentProfile->setting("custom_overwrites", exe.title())
- .toString();
- if (m_CurrentProfile->forcedLibrariesEnabled(exe.title())) {
- forcedLibraries = m_CurrentProfile->determineForcedLibraries(exe.title());
- }
- if (arguments == "") {
- arguments = exe.arguments();
- }
- binary = exe.binaryInfo();
- if (cwd.length() == 0) {
- currentDirectory = exe.workingDirectory();
- }
- } catch (const std::runtime_error &) {
- qWarning("\"%s\" not set up as executable",
- qUtf8Printable(executable));
- binary = QFileInfo(executable);
- }
- }
-
- if (!forcedCustomOverwrite.isEmpty())
- customOverwrite = forcedCustomOverwrite;
- if (ignoreCustomOverwrite)
- customOverwrite.clear();
-
- return spawnBinaryDirect(binary,
- arguments,
- profileName,
- currentDirectory,
- steamAppID,
- customOverwrite,
- forcedLibraries);
-}
-
-bool OrganizerCore::waitForApplication(HANDLE handle, LPDWORD exitCode)
-{
- if (!Settings::instance().lockGUI())
- return true;
-
- ILockedWaitingForProcess* uilock = nullptr;
- if (m_UserInterface != nullptr) {
- uilock = m_UserInterface->lock();
- }
-
- ON_BLOCK_EXIT([&] () {
- if (m_UserInterface != nullptr) {
- m_UserInterface->unlock();
- } });
- return waitForProcessCompletion(handle, exitCode, uilock);
-}
-
-bool OrganizerCore::waitForProcessCompletion(HANDLE handle, LPDWORD exitCode, ILockedWaitingForProcess* uilock)
-{
- bool originalHandle = true;
- bool newHandle = true;
- bool uiunlocked = false;
-
- DWORD currentPID = 0;
- QString processName;
- auto waitForChildUntil = GetTickCount64();
- if (handle != INVALID_HANDLE_VALUE) {
- currentPID = GetProcessId(handle);
- processName = QString::fromStdWString(getProcessName(handle));
- }
-
- // Certain process names we wish to "hide" for aesthetic reason:
- bool waitingOnHidden = false;
- std::vector<QString> hiddenList;
- hiddenList.push_back(QFileInfo(QCoreApplication::applicationFilePath()).fileName());
- for (QString hide : hiddenList)
- if (processName.contains(hide, Qt::CaseInsensitive))
- waitingOnHidden = true;
- // The main reason for adding the hidden list is to hide the MO proxy we use to spawn virtualized processes.
- // On the one hand we want to display the real executable without it feeling laggy, on the other we don't want
- // to requery processes all the time if for some reason we are waiting on hidden processes and find no "unhidden"
- // process. For this reason we use exponential backoff and also start with a delibrately low value to improve
- // the responsiveness of the initial update
- DWORD64 nextHiddenCheck = GetTickCount64();
- DWORD64 nextHiddenCheckDelay = 50;
-
- constexpr DWORD INPUT_EVENT = WAIT_OBJECT_0 + 1;
- DWORD res = WAIT_TIMEOUT;
- while (handle != INVALID_HANDLE_VALUE && (newHandle || res == WAIT_TIMEOUT || res == INPUT_EVENT))
- {
- if (newHandle) {
- processName += QString(" (%1)").arg(currentPID);
- if (uilock)
- uilock->setProcessName(processName);
- qDebug() << "Waiting for"
- << (originalHandle ? "spawned" : "usvfs")
- << "process completion :" << qUtf8Printable(processName);
- newHandle = false;
- }
-
- // Wait for a an event on the handle, a key press, mouse click or timeout
- res = MsgWaitForMultipleObjects(1, &handle, FALSE, 200, QS_KEY | QS_MOUSEBUTTON);
- if (res == WAIT_FAILED) {
- qWarning() << "Failed waiting for process completion : MsgWaitForMultipleObjects WAIT_FAILED" << GetLastError();
- break;
- }
-
- // keep processing events so the app doesn't appear dead
- QCoreApplication::sendPostedEvents();
- QCoreApplication::processEvents();
-
- if (uilock && uilock->unlockForced()) {
- uiunlocked = true;
- break;
- }
-
- if (res == WAIT_OBJECT_0) {
- // process we were waiting on has completed
- if (originalHandle && exitCode && !::GetExitCodeProcess(handle, exitCode))
- qWarning() << "Failed getting exit code of complete process :" << GetLastError();
- CloseHandle(handle);
- handle = INVALID_HANDLE_VALUE;
- originalHandle = false;
- // if the previous process spawned a child process and immediately exits we may miss it if we check immediately
- waitForChildUntil = GetTickCount64() + 800;
- }
-
- // search for another process to wait on if either:
- // 1. we just completed waiting for a process and need to find/wait for an inject child
- // 2. we are currently waiting on a hidden process so periodically check if there is a non-hidden process to wait on
- bool firstIteration = true;
- while ((handle == INVALID_HANDLE_VALUE && GetTickCount64() <= waitForChildUntil)
- || (waitingOnHidden && GetTickCount64() >= nextHiddenCheck))
- {
- if (firstIteration)
- firstIteration = false;
- else {
- QThread::msleep(200);
- QCoreApplication::sendPostedEvents();
- QCoreApplication::processEvents();
- }
-
- // search if there is another usvfs process active
- handle = findAndOpenAUSVFSProcess(hiddenList, currentPID);
- waitingOnHidden = false;
- newHandle = handle != INVALID_HANDLE_VALUE;
- if (newHandle) {
- currentPID = GetProcessId(handle);
- processName = QString::fromStdWString(getProcessName(handle));
- for (QString hide : hiddenList)
- if (processName.contains(hide, Qt::CaseInsensitive))
- waitingOnHidden = true;
- }
- if (waitingOnHidden) {
- nextHiddenCheck = GetTickCount64() + nextHiddenCheckDelay;
- nextHiddenCheckDelay = std::min(nextHiddenCheckDelay * 2, (DWORD64) 2000);
- }
- else {
- nextHiddenCheck = GetTickCount64();
- nextHiddenCheckDelay = 200;
- }
- }
- }
-
- if (res == WAIT_OBJECT_0)
- qDebug() << "Waiting for process completion successfull";
- else if (uiunlocked)
- qDebug() << "Waiting for process completion aborted by UI";
- else
- qDebug() << "Waiting for process completion not successfull :" << res;
-
- if (handle != INVALID_HANDLE_VALUE)
- ::CloseHandle(handle);
-
- return res == WAIT_OBJECT_0;
-}
-
-HANDLE OrganizerCore::findAndOpenAUSVFSProcess(const std::vector<QString>& hiddenList, DWORD preferedParentPid) {
- // for practical reasons a querySize of 1 is probably enough, we use a larger query as a heuristics
- // to find a more "aesthetic injected processes (attempting to comply to hiddenList and preferedParentPid)
- constexpr size_t querySize = 100;
- DWORD pids[querySize];
- size_t found = querySize;
- if (!::GetVFSProcessList(&found, pids)) {
- qWarning() << "Failed seeking USVFS processes : GetVFSProcessList failed?!";
- return INVALID_HANDLE_VALUE;
- }
-
- HANDLE best_match = INVALID_HANDLE_VALUE;
- bool best_match_hidden = true;
- for (size_t i = 0; i < found; ++i) {
- if (pids[i] == GetCurrentProcessId())
- continue; // obviously don't wait for MO process
-
- HANDLE handle = ::OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION | SYNCHRONIZE, FALSE, pids[i]);
- if (handle == INVALID_HANDLE_VALUE) {
- qWarning() << "Failed openning USVFS process " << pids[i] << " : OpenProcess failed" << GetLastError();
- continue;
- }
-
- QString pname = QString::fromStdWString(getProcessName(handle));
- bool phidden = false;
- for (auto hide : hiddenList)
- if (pname.contains(hide, Qt::CaseInsensitive))
- phidden = true;
-
- bool pprefered = preferedParentPid && getProcessParentID(pids[i]) == preferedParentPid;
-
- if (best_match == INVALID_HANDLE_VALUE || best_match_hidden || (!phidden && pprefered)) {
- if (best_match != INVALID_HANDLE_VALUE)
- CloseHandle(best_match);
- best_match = handle;
- best_match_hidden = phidden;
- }
- else
- CloseHandle(handle);
-
- if (!phidden && pprefered)
- return best_match;
- }
-
- return best_match;
-}
-
bool OrganizerCore::onAboutToRun(
const std::function<bool(const QString &)> &func)
{
@@ -2029,8 +1101,10 @@ void OrganizerCore::refreshModList(bool saveChanges)
if (saveChanges) {
m_CurrentProfile->writeModlistNow(true);
}
- ModInfo::updateFromDisc(m_Settings.getModDirectory(), &m_DirectoryStructure,
- m_PluginContainer, m_Settings.displayForeign(), managedGame());
+
+ ModInfo::updateFromDisc(
+ m_Settings.paths().mods(), &m_DirectoryStructure,
+ m_PluginContainer, m_Settings.interface().displayForeign(), managedGame());
m_CurrentProfile->refreshModStatus();
@@ -2118,7 +1192,7 @@ void OrganizerCore::updateModsActiveState(const QList<unsigned int> &modIndices,
dir.entryList(QStringList() << "*.esm", QDir::Files)) {
const FileEntry::Ptr file = m_DirectoryStructure->findFile(ToWString(esm));
if (file.get() == nullptr) {
- qWarning("failed to activate %s", qUtf8Printable(esm));
+ log::warn("failed to activate {}", esm);
continue;
}
@@ -2134,7 +1208,7 @@ void OrganizerCore::updateModsActiveState(const QList<unsigned int> &modIndices,
dir.entryList(QStringList() << "*.esl", QDir::Files)) {
const FileEntry::Ptr file = m_DirectoryStructure->findFile(ToWString(esl));
if (file.get() == nullptr) {
- qWarning("failed to activate %s", qUtf8Printable(esl));
+ log::warn("failed to activate {}", esl);
continue;
}
@@ -2150,7 +1224,7 @@ void OrganizerCore::updateModsActiveState(const QList<unsigned int> &modIndices,
for (const QString &esp : esps) {
const FileEntry::Ptr file = m_DirectoryStructure->findFile(ToWString(esp));
if (file.get() == nullptr) {
- qWarning("failed to activate %s", qUtf8Printable(esp));
+ log::warn("failed to activate {}", esp);
continue;
}
@@ -2199,7 +1273,7 @@ void OrganizerCore::updateModsInDirectoryStructure(QMap<unsigned int, ModInfo::P
updateModsActiveState(modInfo.keys(), true);
m_PluginList.blockSignals(false);
// now we need to refresh the bsa list and save it so there is no confusion
- // about what archives are avaiable and active
+ // about what archives are available and active
refreshBSAList();
if (m_UserInterface != nullptr) {
m_UserInterface->archivesWriter().writeImmediately(false);
@@ -2225,7 +1299,7 @@ void OrganizerCore::loggedInAction(QWidget* parent, std::function<void ()> f)
f();
} else {
QString apiKey;
- if (settings().getNexusApiKey(apiKey)) {
+ if (settings().nexus().apiKey(apiKey)) {
doAfterLogin([f]{ f(); });
NexusInterface::instance(m_PluginContainer)->getAccessManager()->apiCheck(apiKey);
} else {
@@ -2364,8 +1438,10 @@ void OrganizerCore::profileRefresh()
{
// have to refresh mods twice (again in refreshModList), otherwise the refresh
// isn't complete. Not sure why
- ModInfo::updateFromDisc(m_Settings.getModDirectory(), &m_DirectoryStructure,
- m_PluginContainer, m_Settings.displayForeign(), managedGame());
+ ModInfo::updateFromDisc(
+ m_Settings.paths().mods(), &m_DirectoryStructure,
+ m_PluginContainer, m_Settings.interface().displayForeign(), managedGame());
+
m_CurrentProfile->refreshModStatus();
refreshModList();
@@ -2484,6 +1560,9 @@ void OrganizerCore::loginSuccessfulUpdate(bool necessary)
void OrganizerCore::loginFailed(const QString &message)
{
+ qCritical().nospace().noquote()
+ << "Nexus API validation failed: " << message;
+
if (QMessageBox::question(qApp->activeWindow(), tr("Login failed"),
tr("Login failed, try again?"))
== QMessageBox::Yes) {
@@ -2529,7 +1608,7 @@ void OrganizerCore::syncOverwrite()
SyncOverwriteDialog syncDialog(modInfo->absolutePath(), m_DirectoryStructure,
qApp->activeWindow());
if (syncDialog.exec() == QDialog::Accepted) {
- syncDialog.apply(QDir::fromNativeSeparators(m_Settings.getModDirectory()));
+ syncDialog.apply(QDir::fromNativeSeparators(m_Settings.paths().mods()));
modInfo->testValid();
refreshDirectoryStructure();
}
@@ -2552,10 +1631,10 @@ std::vector<unsigned int> OrganizerCore::activeProblems() const
const auto& hookdll = oldMO1HookDll();
if (!hookdll.isEmpty()) {
// This warning will now be shown every time the problems are checked, which is a bit
- // of a "log spam". But since this is a sevre error which will most likely make the
+ // of a "log spam". But since this is a sever error which will most likely make the
// game crash/freeze/etc. and is very hard to diagnose, this "log spam" will make it
// easier for the user to notice the warning.
- qWarning("hook.dll found in game folder: %s", qUtf8Printable(hookdll));
+ log::warn("hook.dll found in game folder: {}", hookdll);
problems.push_back(PROBLEM_MO1SCRIPTEXTENDERWORKAROUND);
}
return problems;
@@ -2601,7 +1680,7 @@ void OrganizerCore::startGuidedFix(unsigned int) const
bool OrganizerCore::saveCurrentLists()
{
if (m_DirectoryUpdate) {
- qWarning("not saving lists during directory update");
+ log::warn("not saving lists during directory update");
return false;
}
@@ -2628,22 +1707,102 @@ void OrganizerCore::savePluginList()
}
m_PluginList.saveTo(m_CurrentProfile->getLockedOrderFileName(),
m_CurrentProfile->getDeleterFileName(),
- m_Settings.hideUncheckedPlugins());
+ m_Settings.game().hideUncheckedPlugins());
m_PluginList.saveLoadOrder(*m_DirectoryStructure);
}
-void OrganizerCore::prepareStart()
+void OrganizerCore::saveCurrentProfile()
{
if (m_CurrentProfile == nullptr) {
return;
}
+
m_CurrentProfile->writeModlist();
m_CurrentProfile->createTweakedIniFile();
saveCurrentLists();
- m_Settings.setupLoadMechanism();
+ m_Settings.game().loadMechanism().activate(m_Settings.game().loadMechanismType());
storeSettings();
}
+ProcessRunner OrganizerCore::processRunner()
+{
+ return ProcessRunner(*this, m_UserInterface);
+}
+
+bool OrganizerCore::beforeRun(
+ const QFileInfo& binary, const QString& profileName,
+ const QString& customOverwrite,
+ const QList<MOBase::ExecutableForcedLoadSetting>& forcedLibraries)
+{
+ saveCurrentProfile();
+
+ while (m_DirectoryUpdate) {
+ ::Sleep(100);
+ QCoreApplication::processEvents();
+ }
+
+ // need to make sure all data is saved before we start the application
+ if (m_CurrentProfile != nullptr) {
+ m_CurrentProfile->writeModlistNow(true);
+ }
+
+ // TODO: should also pass arguments
+ if (!m_AboutToRun(binary.absoluteFilePath())) {
+ log::debug("start of \"{}\" cancelled by plugin", binary.absoluteFilePath());
+ return false;
+ }
+
+ try
+ {
+ m_USVFS.updateMapping(fileMapping(profileName, customOverwrite));
+ m_USVFS.updateForcedLibraries(forcedLibraries);
+ }
+ catch (const UsvfsConnectorException &e)
+ {
+ log::debug(e.what());
+ return false;
+ }
+ catch (const std::exception &e)
+ {
+ QWidget* w = nullptr;
+ if (m_UserInterface) {
+ w = m_UserInterface->qtWidget();
+ }
+ QMessageBox::warning(w, tr("Error"), e.what());
+ return false;
+ }
+
+ return true;
+}
+
+void OrganizerCore::afterRun(const QFileInfo& binary, DWORD exitCode)
+{
+ refreshDirectoryStructure();
+
+ // need to remove our stored load order because it may be outdated if a
+ // foreign tool changed the file time. After removing that file,
+ // refreshESPList will use the file time as the order
+ if (managedGame()->loadOrderMechanism() == IPluginGame::LoadOrderMechanism::FileTime) {
+ log::debug("removing loadorder.txt");
+ QFile::remove(m_CurrentProfile->getLoadOrderFileName());
+ }
+
+ refreshDirectoryStructure();
+
+ refreshESPList(true);
+ savePluginList();
+ cycleDiagnostics();
+
+ //These callbacks should not fiddle with directoy structure and ESPs.
+ m_FinishedRun(binary.absoluteFilePath(), exitCode);
+}
+
+ProcessRunner::Results OrganizerCore::waitForAllUSVFSProcesses(
+ UILocker::Reasons reason)
+{
+ return processRunner().waitForAllUSVFSProcessesWithLock(reason);
+}
+
std::vector<Mapping> OrganizerCore::fileMapping(const QString &profileName,
const QString &customOverwrite)
{
@@ -2654,7 +1813,7 @@ std::vector<Mapping> OrganizerCore::fileMapping(const QString &profileName,
}
IPluginGame *game = qApp->property("managed_game").value<IPluginGame *>();
- Profile profile(QDir(m_Settings.getProfileDirectory() + "/" + profileName),
+ Profile profile(QDir(m_Settings.paths().profiles() + "/" + profileName),
game);
MappingType result;
@@ -2695,12 +1854,12 @@ std::vector<Mapping> OrganizerCore::fileMapping(const QString &profileName,
result.reserve(result.size() + saveMap.size());
result.insert(result.end(), saveMap.begin(), saveMap.end());
} else {
- qWarning("local save games not supported by this game plugin");
+ log::warn("local save games not supported by this game plugin");
}
}
result.insert(result.end(), {
- QDir::toNativeSeparators(m_Settings.getOverwriteDirectory()),
+ QDir::toNativeSeparators(m_Settings.paths().overwrite()),
dataPath,
true,
customOverwrite.isEmpty()
diff --git a/src/organizercore.h b/src/organizercore.h
index 99b1c5f2..6c9edb9f 100644
--- a/src/organizercore.h
+++ b/src/organizercore.h
@@ -3,7 +3,6 @@
#include "selfupdater.h"
-#include "iuserinterface.h" //should be class IUserInterface;
#include "settings.h"
#include "modlist.h"
#include "modinfo.h"
@@ -14,6 +13,8 @@
#include "executableslist.h"
#include "usvfsconnector.h"
#include "moshortcut.h"
+#include "processrunner.h"
+#include "uilocker.h"
#include <directoryentry.h>
#include <imoinfo.h>
#include <iplugindiagnose.h>
@@ -21,10 +22,13 @@
#include <delayedfilewriter.h>
#include <boost/signals2.hpp>
#include "executableinfo.h"
+#include <log.h>
class ModListSortProxy;
class PluginListSortProxy;
class Profile;
+class IUserInterface;
+
namespace MOBase {
template <typename T> class GuessedValue;
class IModInterface;
@@ -88,26 +92,21 @@ private:
typedef boost::signals2::signal<void (const QString&)> SignalModInstalled;
public:
- enum class FileExecutionTypes
- {
- Executable = 1,
- Other = 2
- };
-
static bool isNxmLink(const QString &link) { return link.startsWith("nxm://", Qt::CaseInsensitive); }
- OrganizerCore(const QSettings &initSettings);
+ OrganizerCore(Settings &settings);
~OrganizerCore();
- void setUserInterface(IUserInterface *userInterface, QWidget *widget);
+ void setUserInterface(IUserInterface* ui);
void connectPlugins(PluginContainer *container);
void disconnectPlugins();
void setManagedGame(MOBase::IPluginGame *game);
- void updateExecutablesList(QSettings &settings);
+ void updateExecutablesList();
+ void checkForUpdates();
void startMOUpdate();
Settings &settings();
@@ -136,7 +135,17 @@ public:
bool saveCurrentLists();
- void prepareStart();
+ ProcessRunner processRunner();
+
+ bool beforeRun(
+ const QFileInfo& binary, const QString& profileName,
+ const QString& customOverwrite,
+ const QList<MOBase::ExecutableForcedLoadSetting>& forcedLibraries);
+
+ void afterRun(const QFileInfo& binary, DWORD exitCode);
+
+ ProcessRunner::Results waitForAllUSVFSProcesses(
+ UILocker::Reasons reason=UILocker::PreventExit);
void refreshESPList(bool force = false);
void refreshBSAList();
@@ -148,37 +157,9 @@ public:
void doAfterLogin(const std::function<void()> &function) { m_PostLoginTasks.append(function); }
void loggedInAction(QWidget* parent, std::function<void ()> f);
- static QString findJavaInstallation(const QString& jarFile={});
-
- static bool getFileExecutionContext(
- QWidget* parent, const QFileInfo &targetInfo,
- QFileInfo &binaryInfo, QString &arguments, FileExecutionTypes& type);
-
- bool executeFileVirtualized(QWidget* parent, const QFileInfo& targetInfo);
bool previewFileWithAlternatives(QWidget* parent, QString filename, int selectedOrigin=-1);
bool previewFile(QWidget* parent, const QString& originName, const QString& path);
- void spawnBinary(const QFileInfo &binary, const QString &arguments = "",
- const QDir &currentDirectory = QDir(),
- const QString &steamAppID = "",
- const QString &customOverwrite = "",
- const QList<MOBase::ExecutableForcedLoadSetting> &forcedLibraries = QList<MOBase::ExecutableForcedLoadSetting>());
-
- HANDLE spawnBinaryDirect(const QFileInfo &binary, const QString &arguments,
- const QString &profileName,
- const QDir &currentDirectory,
- const QString &steamAppID,
- const QString &customOverwrite,
- const QList<MOBase::ExecutableForcedLoadSetting> &forcedLibraries = QList<MOBase::ExecutableForcedLoadSetting>(),
- LPDWORD exitCode = nullptr);
-
- HANDLE spawnBinaryProcess(const QFileInfo &binary, const QString &arguments,
- const QString &profileName,
- const QDir &currentDirectory,
- const QString &steamAppID,
- const QString &customOverwrite,
- const QList<MOBase::ExecutableForcedLoadSetting> &forcedLibraries = QList<MOBase::ExecutableForcedLoadSetting>());
-
void loginSuccessfulUpdate(bool necessary);
void loginFailedUpdate(const QString &message);
@@ -191,12 +172,17 @@ public:
void prepareVFS();
- void updateVFSParams(int logLevel, int crashDumpsType, QString executableBlacklist);
+ void updateVFSParams(
+ MOBase::log::Levels logLevel, CrashDumpsType crashDumpsType,
+ const QString& crashDumpsPath, std::chrono::seconds spawnDelay,
+ QString executableBlacklist);
+
+ void setLogLevel(MOBase::log::Levels level);
bool cycleDiagnostics();
static CrashDumpsType getGlobalCrashDumpsType() { return m_globalCrashDumpsType; }
- static void setGlobalCrashDumpsType(int crashDumpsType);
+ static void setGlobalCrashDumpsType(CrashDumpsType crashDumpsType);
static std::wstring crashDumpsPath();
public:
@@ -227,10 +213,6 @@ public:
DownloadManager *downloadManager();
PluginList *pluginList();
ModList *modList();
- HANDLE runShortcut(const MOShortcut& shortcut);
- HANDLE startApplication(const QString &executable, const QStringList &args, const QString &cwd, const QString &profile, const QString &forcedCustomOverwrite = "", bool ignoreCustomOverwrite = false);
- bool waitForApplication(HANDLE processHandle, LPDWORD exitCode = nullptr);
- HANDLE findAndOpenAUSVFSProcess(const std::vector<QString>& hiddenList, DWORD preferedParentPid);
bool onModInstalled(const std::function<void (const QString &)> &func);
bool onAboutToRun(const std::function<bool (const QString &)> &func);
bool onFinishedRun(const std::function<void (const QString &, unsigned int)> &func);
@@ -281,12 +263,9 @@ signals:
private:
+ void saveCurrentProfile();
void storeSettings();
- QSettings::Status storeSettings(const QString &fileName);
-
- QString commitSettings(const QString &iniFile);
-
bool queryApi(QString &apiKey);
void updateModActiveState(int index, bool active);
@@ -310,8 +289,6 @@ private:
const MOShared::DirectoryEntry *directoryEntry,
int createDestination);
- bool waitForProcessCompletion(HANDLE handle, LPDWORD exitCode, ILockedWaitingForProcess* uilock);
-
private slots:
void directory_refreshed();
@@ -325,15 +302,14 @@ private:
static const unsigned int PROBLEM_MO1SCRIPTEXTENDERWORKAROUND = 1;
private:
-
- IUserInterface *m_UserInterface;
+ IUserInterface* m_UserInterface;
PluginContainer *m_PluginContainer;
QString m_GameName;
MOBase::IPluginGame *m_GamePlugin;
Profile *m_CurrentProfile;
- Settings m_Settings;
+ Settings& m_Settings;
SelfUpdater m_Updater;
@@ -368,6 +344,8 @@ private:
MOBase::DelayedFileWriter m_PluginListsWriter;
UsvfsConnector m_USVFS;
+ UILocker m_UILocker;
+
static CrashDumpsType m_globalCrashDumpsType;
};
diff --git a/src/organizerproxy.cpp b/src/organizerproxy.cpp
index d6996b3a..613de742 100644
--- a/src/organizerproxy.cpp
+++ b/src/organizerproxy.cpp
@@ -107,15 +107,65 @@ QString OrganizerProxy::pluginDataPath() const
return m_Proxied->pluginDataPath();
}
-HANDLE OrganizerProxy::startApplication(const QString &executable, const QStringList &args, const QString &cwd,
- const QString &profile, const QString &forcedCustomOverwrite, bool ignoreCustomOverwrite)
+HANDLE OrganizerProxy::startApplication(
+ const QString& exe, const QStringList& args, const QString &cwd,
+ const QString& profile, const QString &overwrite, bool ignoreOverwrite)
{
- return m_Proxied->startApplication(executable, args, cwd, profile, forcedCustomOverwrite, ignoreCustomOverwrite);
+ log::debug(
+ "a plugin has requested to start an application:\n"
+ " . executable: '{}'\n"
+ " . args: '{}'\n"
+ " . cwd: '{}'\n"
+ " . profile: '{}'\n"
+ " . overwrite: '{}'\n"
+ " . ignore overwrite: {}",
+ exe, args.join(" "), cwd, profile, overwrite, ignoreOverwrite);
+
+ auto runner = m_Proxied->processRunner();
+
+ // don't wait for completion
+ runner
+ .setFromFileOrExecutable(exe, args, cwd, profile, overwrite, ignoreOverwrite)
+ .run();
+
+ // the plugin is in charge of closing the handle, unless waitForApplication()
+ // is called on it
+ return runner.stealProcessHandle().release();
}
bool OrganizerProxy::waitForApplication(HANDLE handle, LPDWORD exitCode) const
{
- return m_Proxied->waitForApplication(handle, exitCode);
+ const auto pid = ::GetProcessId(handle);
+
+ log::debug(
+ "a plugin wants to wait for an application to complete, pid {}{}",
+ pid, (pid == 0 ? "unknown (probably already completed)" : ""));
+
+ auto runner = m_Proxied->processRunner();
+
+ const auto r = runner
+ .setWaitForCompletion(ProcessRunner::ForceWait, UILocker::OutputRequired)
+ .attachToProcess(handle);
+
+ if (exitCode) {
+ *exitCode = runner.exitCode();
+ }
+
+ switch (r)
+ {
+ case ProcessRunner::Completed:
+ return true;
+
+ case ProcessRunner::Cancelled: // fall-through
+ case ProcessRunner::ForceUnlocked:
+ // this is always an error because the application should have run to
+ // completion
+ return false;
+
+ case ProcessRunner::Error: // fall-through
+ default:
+ return false;
+ }
}
bool OrganizerProxy::onAboutToRun(const std::function<bool (const QString &)> &func)
diff --git a/src/overwriteinfodialog.cpp b/src/overwriteinfodialog.cpp
index 5ee8d76c..ab726fb3 100644
--- a/src/overwriteinfodialog.cpp
+++ b/src/overwriteinfodialog.cpp
@@ -32,7 +32,6 @@ using namespace MOBase;
class MyFileSystemModel : public QFileSystemModel {
-
public:
MyFileSystemModel(QObject *parent)
: QFileSystemModel(parent), m_RegularColumnCount(0) {}
@@ -105,6 +104,29 @@ OverwriteInfoDialog::~OverwriteInfoDialog()
delete ui;
}
+void OverwriteInfoDialog::showEvent(QShowEvent* e)
+{
+ const auto& s = Settings::instance();
+
+ s.geometry().restoreGeometry(this);
+
+ if (!s.geometry().restoreState(ui->filesView->header())) {
+ ui->filesView->sortByColumn(0, Qt::AscendingOrder);
+ }
+
+ QDialog::showEvent(e);
+}
+
+void OverwriteInfoDialog::done(int r)
+{
+ auto& s = Settings::instance();
+
+ s.geometry().saveGeometry(this);
+ s.geometry().saveState(ui->filesView->header());
+
+ QDialog::done(r);
+}
+
void OverwriteInfoDialog::setModInfo(ModInfo::Ptr modInfo)
{
m_ModInfo = modInfo;
@@ -121,18 +143,18 @@ bool OverwriteInfoDialog::recursiveDelete(const QModelIndex &index)
QModelIndex childIndex = m_FileSystemModel->index(childRow, 0, index);
if (m_FileSystemModel->isDir(childIndex)) {
if (!recursiveDelete(childIndex)) {
- qCritical("failed to delete %s", m_FileSystemModel->fileName(childIndex).toUtf8().constData());
+ log::error("failed to delete {}", m_FileSystemModel->fileName(childIndex));
return false;
}
} else {
if (!m_FileSystemModel->remove(childIndex)) {
- qCritical("failed to delete %s", m_FileSystemModel->fileName(childIndex).toUtf8().constData());
+ log::error("failed to delete {}", m_FileSystemModel->fileName(childIndex));
return false;
}
}
}
if (!m_FileSystemModel->remove(index)) {
- qCritical("failed to delete %s", m_FileSystemModel->fileName(index).toUtf8().constData());
+ log::error("failed to delete {}", m_FileSystemModel->fileName(index));
return false;
}
return true;
@@ -218,7 +240,7 @@ void OverwriteInfoDialog::renameTriggered()
void OverwriteInfoDialog::openFile(const QModelIndex &index)
{
- shell::OpenFile(m_FileSystemModel->filePath(index));
+ shell::Open(m_FileSystemModel->filePath(index));
}
@@ -259,7 +281,7 @@ void OverwriteInfoDialog::createDirectoryTriggered()
void OverwriteInfoDialog::on_explorerButton_clicked()
{
- shell::ExploreFile(m_ModInfo->absolutePath());
+ shell::Explore(m_ModInfo->absolutePath());
}
void OverwriteInfoDialog::on_filesView_customContextMenuRequested(const QPoint &pos)
diff --git a/src/overwriteinfodialog.h b/src/overwriteinfodialog.h
index 4b731736..bedb779a 100644
--- a/src/overwriteinfodialog.h
+++ b/src/overwriteinfodialog.h
@@ -31,7 +31,7 @@ class OverwriteInfoDialog;
class OverwriteInfoDialog : public QDialog
{
Q_OBJECT
-
+
public:
explicit OverwriteInfoDialog(ModInfo::Ptr modInfo, QWidget *parent = 0);
@@ -39,8 +39,17 @@ public:
ModInfo::Ptr modInfo() const { return m_ModInfo; }
+ // saves geometry
+ //
+ void done(int r) override;
+
void setModInfo(ModInfo::Ptr modInfo);
+protected:
+ // restores geometry
+ //
+ void showEvent(QShowEvent* e) override;
+
private:
void openFile(const QModelIndex &index);
diff --git a/src/pch.h b/src/pch.h
index 1d8df43a..b7c5d695 100644
--- a/src/pch.h
+++ b/src/pch.h
@@ -91,17 +91,18 @@
#include <QDateTime>
#include <QDebug>
#include <QDesktopServices>
-#include <QDesktopWidget>
#include <QDialog>
#include <QDialogButtonBox>
#include <QDir>
#include <QDirIterator>
+#include <QDockWidget>
#include <QDragEnterEvent>
#include <QDropEvent>
+#include <QElapsedTimer>
#include <QEvent>
+#include <QFIleIconProvider>
#include <QFile>
#include <QFileDialog>
-#include <QFIleIconProvider>
#include <QFileInfo>
#include <QFileSystemModel>
#include <QFileSystemWatcher>
@@ -110,13 +111,14 @@
#include <QFontDatabase>
#include <QFuture>
#include <QFutureWatcher>
-#include <QHash>
+#include <QGraphicsDropShadowEffect>
#include <QHBoxLayout>
+#include <QHash>
#include <QHeaderView>
+#include <QIODevice>
#include <QIcon>
#include <QImageReader>
#include <QInputDialog>
-#include <QIODevice>
#include <QItemDelegate>
#include <QItemSelection>
#include <QItemSelectionModel>
@@ -125,20 +127,21 @@
#include <QJsonObject>
#include <QJsonValueRef>
#include <QKeyEvent>
-#include <QLabel>
#include <QLCDNumber>
+#include <QLabel>
#include <QLibrary>
#include <QLibraryInfo>
#include <QLineEdit>
#include <QList>
#include <QListWidget>
#include <QListWidgetItem>
-#include <QLocale>
#include <QLocalServer>
#include <QLocalSocket>
+#include <QLocale>
#include <QMainWindow>
#include <QMap>
#include <QMenu>
+#include <QMenuBar>
#include <QMessageBox>
#include <QMetaType>
#include <QMimeData>
@@ -191,50 +194,40 @@
#include <QSortFilterProxyModel>
#include <QSpinBox>
#include <QSplashScreen>
+#include <QSplitter>
#include <QSslSocket>
#include <QStandardItemModel>
-#include <qstandardpaths.h>
#include <QStandardPaths>
+#include <QStatusBar>
+#include <QStorageInfo>
#include <QString>
#include <QStringList>
#include <QStringListModel>
#include <QStyle>
-#include <QStyledItemDelegate>
#include <QStyleFactory>
#include <QStyleOption>
#include <QStyleOptionSlider>
+#include <QStyledItemDelegate>
#include <QSyntaxHighlighter>
-#include <Qt>
-#include <QTableWidget>
#include <QTabWidget>
-#include <QtAlgorithms>
-#include <QtConcurrent/QtConcurrentRun>
-#include <QtCore/QAbstractItemModel>
-#include <QtCore/QObject>
-#include <QtCore/QStack>
-#include <QtDebug>
+#include <QTableWidget>
#include <QTemporaryFile>
#include <QTextBrowser>
#include <QTextCodec>
#include <QTextDocument>
#include <QTextEdit>
#include <QTextStream>
-#include <QtGlobal>
-#include <QtGui/QtGui>
#include <QThread>
#include <QTime>
#include <QTimer>
#include <QToolBar>
#include <QToolButton>
#include <QToolTip>
-#include <QtPlatformHeaders/QWindowsWindowFunctions>
-#include <QtPlugin>
#include <QTranslator>
#include <QTreeView>
#include <QTreeWidget>
#include <QTreeWidgetItem>
#include <QTreeWidgetItemIterator>
-#include <QtTest/QtTest>
#include <QUrl>
#include <QUrlQuery>
#include <QVariant>
@@ -253,3 +246,16 @@
#include <QWhatsThisClickedEvent>
#include <QWidget>
#include <QWidgetAction>
+#include <Qt>
+#include <QtAlgorithms>
+#include <QtConcurrent/QtConcurrentRun>
+#include <QtCore/QAbstractItemModel>
+#include <QtCore/QObject>
+#include <QtCore/QStack>
+#include <QtDebug>
+#include <QtGlobal>
+#include <QtGui/QtGui>
+#include <QtPlatformHeaders/QWindowsWindowFunctions>
+#include <QtPlugin>
+#include <QtTest/QtTest>
+#include <QStandardPaths>
diff --git a/src/persistentcookiejar.cpp b/src/persistentcookiejar.cpp
index 1ed463c6..8657f356 100644
--- a/src/persistentcookiejar.cpp
+++ b/src/persistentcookiejar.cpp
@@ -1,8 +1,10 @@
#include "persistentcookiejar.h"
+#include <log.h>
#include <QTemporaryFile>
#include <QDataStream>
#include <QNetworkCookie>
+using namespace MOBase;
PersistentCookieJar::PersistentCookieJar(const QString &fileName, QObject *parent)
: QNetworkCookieJar(parent), m_FileName(fileName)
@@ -11,7 +13,7 @@ PersistentCookieJar::PersistentCookieJar(const QString &fileName, QObject *paren
}
PersistentCookieJar::~PersistentCookieJar() {
- qDebug("save %s", qUtf8Printable(m_FileName));
+ log::debug("save {}", m_FileName);
save();
}
@@ -24,7 +26,7 @@ void PersistentCookieJar::clear() {
void PersistentCookieJar::save() {
QTemporaryFile file;
if (!file.open()) {
- qCritical("failed to save cookies: couldn't create temporary file");
+ log::error("failed to save cookies: couldn't create temporary file");
return;
}
QDataStream data(&file);
@@ -40,14 +42,14 @@ void PersistentCookieJar::save() {
QFile oldCookies(m_FileName);
if (oldCookies.exists()) {
if (!oldCookies.remove()) {
- qCritical("failed to save cookies: failed to remove %s", qUtf8Printable(m_FileName));
+ log::error("failed to save cookies: failed to remove {}", m_FileName);
return;
}
} // if it doesn't exists that's fine
}
if (!file.copy(m_FileName)) {
- qCritical("failed to save cookies: failed to write %s", qUtf8Printable(m_FileName));
+ log::error("failed to save cookies: failed to write {}", m_FileName);
}
}
diff --git a/src/plugincontainer.cpp b/src/plugincontainer.cpp
index 2126c5ef..767d3eb8 100644
--- a/src/plugincontainer.cpp
+++ b/src/plugincontainer.cpp
@@ -3,6 +3,7 @@
#include "organizerproxy.h"
#include "report.h"
#include <ipluginproxy.h>
+#include <iuserinterface.h>
#include <idownloadmanager.h>
#include <appconfig.h>
#include <QAction>
@@ -69,7 +70,7 @@ bool PluginContainer::verifyPlugin(IPlugin *plugin)
if (plugin == nullptr) {
return false;
} else if (!plugin->init(new OrganizerProxy(m_Organizer, this, plugin->name()))) {
- qWarning("plugin failed to initialize");
+ log::warn("plugin failed to initialize");
return false;
}
return true;
@@ -91,11 +92,11 @@ bool PluginContainer::registerPlugin(QObject *plugin, const QString &fileName)
{ // generic treatment for all plugins
IPlugin *pluginObj = qobject_cast<IPlugin*>(plugin);
if (pluginObj == nullptr) {
- qDebug("not an IPlugin");
+ log::debug("not an IPlugin");
return false;
}
plugin->setProperty("filename", fileName);
- m_Organizer->settings().registerPlugin(pluginObj);
+ m_Organizer->settings().plugins().registerPlugin(pluginObj);
}
{ // diagnosis plugin
@@ -164,12 +165,13 @@ bool PluginContainer::registerPlugin(QObject *plugin, const QString &fileName)
for (QObject *proxiedPlugin : matchingPlugins) {
if (proxiedPlugin != nullptr) {
if (registerPlugin(proxiedPlugin, pluginName)) {
- qDebug("loaded plugin \"%s\"", qUtf8Printable(QFileInfo(pluginName).fileName()));
+ log::debug("loaded plugin \"{}\"", QFileInfo(pluginName).fileName());
}
else {
- qWarning("plugin \"%s\" failed to load. If this plugin is for an older version of MO "
- "you have to update it or delete it if no update exists.",
- qUtf8Printable(pluginName));
+ log::warn(
+ "plugin \"{}\" failed to load. If this plugin is for an older version of MO "
+ "you have to update it or delete it if no update exists.",
+ pluginName);
}
}
}
@@ -190,7 +192,7 @@ bool PluginContainer::registerPlugin(QObject *plugin, const QString &fileName)
}
}
- qDebug("no matching plugin interface");
+ log::debug("no matching plugin interface");
return false;
}
@@ -224,7 +226,7 @@ void PluginContainer::unloadPlugins()
QPluginLoader *loader = m_PluginLoaders.back();
m_PluginLoaders.pop_back();
if ((loader != nullptr) && !loader->unload()) {
- qDebug("failed to unload %s: %s", qUtf8Printable(loader->fileName()), qUtf8Printable(loader->errorString()));
+ log::debug("failed to unload {}: {}", loader->fileName(), loader->errorString());
}
delete loader;
}
@@ -265,7 +267,7 @@ void PluginContainer::loadPlugins()
"(Please note: If this is the first time you see this message for this plugin you may want to give it another try. "
"The plugin may be able to recover from the problem)").arg(fileName),
QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes) == QMessageBox::Yes) {
- m_Organizer->settings().addBlacklistPlugin(fileName);
+ m_Organizer->settings().plugins().addBlacklist(fileName);
}
loadCheck.close();
}
@@ -273,13 +275,13 @@ void PluginContainer::loadPlugins()
loadCheck.open(QIODevice::WriteOnly);
QString pluginPath = qApp->applicationDirPath() + "/" + ToQString(AppConfig::pluginPath());
- qDebug("looking for plugins in %s", QDir::toNativeSeparators(pluginPath).toUtf8().constData());
+ log::debug("looking for plugins in {}", QDir::toNativeSeparators(pluginPath));
QDirIterator iter(pluginPath, QDir::Files | QDir::NoDotAndDotDot);
while (iter.hasNext()) {
iter.next();
- if (m_Organizer->settings().pluginBlacklisted(iter.fileName())) {
- qDebug("plugin \"%s\" blacklisted", qUtf8Printable(iter.fileName()));
+ if (m_Organizer->settings().plugins().blacklisted(iter.fileName())) {
+ log::debug("plugin \"{}\" blacklisted", iter.fileName());
continue;
}
loadCheck.write(iter.fileName().toUtf8());
@@ -290,15 +292,16 @@ void PluginContainer::loadPlugins()
std::unique_ptr<QPluginLoader> pluginLoader(new QPluginLoader(pluginName, this));
if (pluginLoader->instance() == nullptr) {
m_FailedPlugins.push_back(pluginName);
- qCritical("failed to load plugin %s: %s",
- qUtf8Printable(pluginName), qUtf8Printable(pluginLoader->errorString()));
+ log::error(
+ "failed to load plugin {}: {}",
+ pluginName, pluginLoader->errorString());
} else {
if (registerPlugin(pluginLoader->instance(), pluginName)) {
- qDebug("loaded plugin \"%s\"", qUtf8Printable(QFileInfo(pluginName).fileName()));
+ log::debug("loaded plugin \"{}\"", QFileInfo(pluginName).fileName());
m_PluginLoaders.push_back(pluginLoader.release());
} else {
m_FailedPlugins.push_back(pluginName);
- qWarning("plugin \"%s\" failed to load (may be outdated)", qUtf8Printable(pluginName));
+ log::warn("plugin \"{}\" failed to load (may be outdated)", pluginName);
}
}
}
diff --git a/src/pluginlist.cpp b/src/pluginlist.cpp
index 2edb92f5..3f2f4018 100644
--- a/src/pluginlist.cpp
+++ b/src/pluginlist.cpp
@@ -56,33 +56,40 @@ using namespace MOBase;
using namespace MOShared;
-static bool ByName(const PluginList::ESPInfo& LHS, const PluginList::ESPInfo& RHS) {
- return LHS.m_Name.toUpper() < RHS.m_Name.toUpper();
+static bool ByName(const PluginList::ESPInfo& LHS, const PluginList::ESPInfo& RHS)
+{
+ return LHS.name.toUpper() < RHS.name.toUpper();
}
-static bool ByPriority(const PluginList::ESPInfo& LHS, const PluginList::ESPInfo& RHS) {
- if (LHS.m_IsMaster && !RHS.m_IsMaster) {
+static bool ByPriority(const PluginList::ESPInfo& LHS, const PluginList::ESPInfo& RHS)
+{
+ if (LHS.isMaster && !RHS.isMaster) {
return true;
- } else if (!LHS.m_IsMaster && RHS.m_IsMaster) {
+ } else if (!LHS.isMaster && RHS.isMaster) {
return false;
} else {
- return LHS.m_Priority < RHS.m_Priority;
+ return LHS.priority < RHS.priority;
}
}
-static bool ByDate(const PluginList::ESPInfo& LHS, const PluginList::ESPInfo& RHS) {
- return QFileInfo(LHS.m_FullPath).lastModified() < QFileInfo(RHS.m_FullPath).lastModified();
+static bool ByDate(const PluginList::ESPInfo& LHS, const PluginList::ESPInfo& RHS)
+{
+ return QFileInfo(LHS.fullPath).lastModified() < QFileInfo(RHS.fullPath).lastModified();
}
-static QString TruncateString(const QString& text) {
+static QString TruncateString(const QString& text)
+{
QString new_text = text;
+
if (new_text.length() > 1024) {
new_text.truncate(1024);
new_text += "...";
}
+
return new_text;
}
+
PluginList::PluginList(QObject *parent)
: QAbstractItemModel(parent)
, m_FontMetrics(QFont())
@@ -113,10 +120,11 @@ QString PluginList::getColumnName(int column)
QString PluginList::getColumnToolTip(int column)
{
switch (column) {
- case COL_NAME: return tr("Name of your mods");
- case COL_PRIORITY: return tr("Load priority of your mod. The higher, the more \"important\" it is and thus "
+ case COL_NAME: return tr("Name of the plugin");
+ case COL_FLAGS: return tr("Emblems to highlight things that might require attention.");
+ case COL_PRIORITY: return tr("Load priority of plugins. The higher, the more \"important\" it is and thus "
"overwrites data from plugins with lower priority.");
- case COL_MODINDEX: return tr("The modindex determines the formids of objects originating from this mods.");
+ case COL_MODINDEX: return tr("Determines the formids of objects originating from this mods.");
default: return tr("unknown");
}
}
@@ -124,8 +132,9 @@ QString PluginList::getColumnToolTip(int column)
void PluginList::highlightPlugins(const QItemSelectionModel *selection, const MOShared::DirectoryEntry &directoryEntry, const Profile &profile)
{
for (auto &esp : m_ESPs) {
- esp.m_ModSelected = false;
+ esp.modSelected = false;
}
+
for (QModelIndex idx : selection->selectedRows(ModList::COL_PRIORITY)) {
int modIndex = idx.data(Qt::UserRole + 1).toInt();
if (modIndex == UINT_MAX)
@@ -146,12 +155,13 @@ void PluginList::highlightPlugins(const QItemSelectionModel *selection, const MO
}
std::map<QString, int>::iterator iter = m_ESPsByName.find(plugin.toLower());
if (iter != m_ESPsByName.end()) {
- m_ESPs[iter->second].m_ModSelected = true;
+ m_ESPs[iter->second].modSelected = true;
}
}
}
}
}
+
emit dataChanged(this->index(0, 0), this->index(static_cast<int>(m_ESPs.size()) - 1, this->columnCount() - 1));
}
@@ -169,6 +179,7 @@ void PluginList::refresh(const QString &profileName
ChangeBracket<PluginList> layoutChange(this);
QStringList primaryPlugins = m_GamePlugin->primaryPlugins();
+ bool lightPluginsAreSupported = m_GamePlugin->feature<GamePlugins>()->lightPluginsAreSupported();
m_CurrentProfile = profileName;
@@ -191,7 +202,7 @@ void PluginList::refresh(const QString &profileName
continue;
}
- bool forceEnabled = Settings::instance().forceEnableCoreFiles() &&
+ bool forceEnabled = Settings::instance().game().forceEnableCoreFiles() &&
primaryPlugins.contains(filename, Qt::CaseInsensitive);
//(std::find(primaryPlugins.begin(), primaryPlugins.end(), filename.toLower()) != primaryPlugins.end());
@@ -222,8 +233,8 @@ void PluginList::refresh(const QString &profileName
originName = modInfo->name();
}
- m_ESPs.push_back(ESPInfo(filename, forceEnabled, originName, ToQString(current->getFullPath()), hasIni, loadedArchives));
- m_ESPs.rbegin()->m_Priority = -1;
+ m_ESPs.push_back(ESPInfo(filename, forceEnabled, originName, ToQString(current->getFullPath()), hasIni, loadedArchives, lightPluginsAreSupported));
+ m_ESPs.rbegin()->priority = -1;
} catch (const std::exception &e) {
reportError(tr("failed to update esp info for file %1 (source id: %2), error: %3").arg(filename).arg(current->getOrigin(archive)).arg(e.what()));
}
@@ -232,13 +243,13 @@ void PluginList::refresh(const QString &profileName
for (const auto &espName : m_ESPsByName) {
if (!availablePlugins.contains(espName.first)) {
- m_ESPs[espName.second].m_Name = "";
+ m_ESPs[espName.second].name = "";
}
}
m_ESPs.erase(std::remove_if(m_ESPs.begin(), m_ESPs.end(),
[](const ESPInfo &info) -> bool {
- return info.m_Name.isEmpty();
+ return info.name.isEmpty();
}),
m_ESPs.end());
@@ -271,7 +282,7 @@ void PluginList::fixPriorities()
std::vector<std::pair<int, int>> espPrios;
for (int i = 0; i < m_ESPs.size(); ++i) {
- int prio = m_ESPs[i].m_Priority;
+ int prio = m_ESPs[i].priority;
if (prio == -1) {
prio = INT_MAX;
}
@@ -284,7 +295,7 @@ void PluginList::fixPriorities()
});
for (int i = 0; i < espPrios.size(); ++i) {
- m_ESPs[espPrios[i].second].m_Priority = i;
+ m_ESPs[espPrios[i].second].priority = i;
}
}
@@ -293,8 +304,8 @@ void PluginList::enableESP(const QString &name, bool enable)
std::map<QString, int>::iterator iter = m_ESPsByName.find(name.toLower());
if (iter != m_ESPsByName.end()) {
- m_ESPs[iter->second].m_Enabled =
- enable | m_ESPs[iter->second].m_ForceEnabled;
+ m_ESPs[iter->second].enabled =
+ enable | m_ESPs[iter->second].forceEnabled;
emit writePluginsList();
} else {
@@ -305,11 +316,11 @@ void PluginList::enableESP(const QString &name, bool enable)
int PluginList::findPluginByPriority(int priority)
{
for (int i = 0; i < m_ESPs.size(); i++ ) {
- if (m_ESPs[i].m_Priority == priority) {
+ if (m_ESPs[i].priority == priority) {
return i;
}
}
- qCritical(QString("No plugin with priority %1").arg(priority).toLocal8Bit());
+ log::error("No plugin with priority {}", priority);
return -1;
}
@@ -319,8 +330,8 @@ void PluginList::enableSelected(const QItemSelectionModel *selectionModel)
bool dirty = false;
for (auto row : selectionModel->selectedRows(COL_PRIORITY)) {
int rowIndex = findPluginByPriority(row.data().toInt());
- if (!m_ESPs[rowIndex].m_Enabled) {
- m_ESPs[rowIndex].m_Enabled = true;
+ if (!m_ESPs[rowIndex].enabled) {
+ m_ESPs[rowIndex].enabled = true;
dirty = true;
}
}
@@ -334,8 +345,8 @@ void PluginList::disableSelected(const QItemSelectionModel *selectionModel)
bool dirty = false;
for (auto row : selectionModel->selectedRows(COL_PRIORITY)) {
int rowIndex = findPluginByPriority(row.data().toInt());
- if (!m_ESPs[rowIndex].m_ForceEnabled && m_ESPs[rowIndex].m_Enabled) {
- m_ESPs[rowIndex].m_Enabled = false;
+ if (!m_ESPs[rowIndex].forceEnabled && m_ESPs[rowIndex].enabled) {
+ m_ESPs[rowIndex].enabled = false;
dirty = true;
}
}
@@ -349,7 +360,7 @@ void PluginList::enableAll()
if (QMessageBox::question(nullptr, tr("Confirm"), tr("Really enable all plugins?"),
QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) {
for (ESPInfo &info : m_ESPs) {
- info.m_Enabled = true;
+ info.enabled = true;
}
emit writePluginsList();
}
@@ -361,8 +372,8 @@ void PluginList::disableAll()
if (QMessageBox::question(nullptr, tr("Confirm"), tr("Really disable all plugins?"),
QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) {
for (ESPInfo &info : m_ESPs) {
- if (!info.m_ForceEnabled) {
- info.m_Enabled = false;
+ if (!info.forceEnabled) {
+ info.enabled = false;
}
}
emit writePluginsList();
@@ -375,7 +386,7 @@ void PluginList::sendToPriority(const QItemSelectionModel *selectionModel, int n
std::vector<int> pluginsToMove;
for (auto row: selectionModel->selectedRows(COL_PRIORITY)) {
int rowIndex = findPluginByPriority(row.data().toInt());
- if (!m_ESPs[rowIndex].m_ForceEnabled) {
+ if (!m_ESPs[rowIndex].forceEnabled) {
pluginsToMove.push_back(rowIndex);
}
}
@@ -390,7 +401,7 @@ bool PluginList::isEnabled(const QString &name)
std::map<QString, int>::iterator iter = m_ESPsByName.find(name.toLower());
if (iter != m_ESPsByName.end()) {
- return m_ESPs[iter->second].m_Enabled;
+ return m_ESPs[iter->second].enabled;
} else {
return false;
}
@@ -401,7 +412,7 @@ void PluginList::clearInformation(const QString &name)
std::map<QString, int>::iterator iter = m_ESPsByName.find(name.toLower());
if (iter != m_ESPsByName.end()) {
- m_AdditionalInfo[name.toLower()].m_Messages.clear();
+ m_AdditionalInfo[name.toLower()].messages.clear();
}
}
@@ -415,15 +426,26 @@ void PluginList::addInformation(const QString &name, const QString &message)
std::map<QString, int>::iterator iter = m_ESPsByName.find(name.toLower());
if (iter != m_ESPsByName.end()) {
- m_AdditionalInfo[name.toLower()].m_Messages.append(message);
+ m_AdditionalInfo[name.toLower()].messages.append(message);
} else {
- qWarning("failed to associate message for \"%s\"", qUtf8Printable(name));
+ log::warn("failed to associate message for \"{}\"", name);
+ }
+}
+
+void PluginList::addLootReport(const QString& name, Loot::Plugin plugin)
+{
+ auto iter = m_ESPsByName.find(name.toLower());
+
+ if (iter != m_ESPsByName.end()) {
+ m_AdditionalInfo[name.toLower()].loot = std::move(plugin);
+ } else {
+ log::warn("failed to associate loot report for \"{}\"", name);
}
}
bool PluginList::isEnabled(int index)
{
- return m_ESPs.at(index).m_Enabled;
+ return m_ESPs.at(index).enabled;
}
void PluginList::readLockedOrderFrom(const QString &fileName)
@@ -437,7 +459,10 @@ void PluginList::readLockedOrderFrom(const QString &fileName)
}
file.open(QIODevice::ReadOnly);
+
+ int lineNumber = 0;
while (!file.atEnd()) {
+ ++lineNumber;
QByteArray line = file.readLine();
if ((line.size() > 0) && (line.at(0) != '#')) {
QList<QByteArray> fields = line.split('|');
@@ -445,15 +470,15 @@ void PluginList::readLockedOrderFrom(const QString &fileName)
int priority = fields.at(1).trimmed().toInt();
QString name = QString::fromUtf8(fields.at(0));
// Avoid locking a force-enabled plugin
- if (!m_ESPs[m_ESPsByName.at(name)].m_ForceEnabled) {
+ if (!m_ESPs[m_ESPsByName.at(name)].forceEnabled) {
// Is this an open and unclaimed priority?
- if (m_ESPs[m_ESPsByPriority.at(priority)].m_ForceEnabled ||
+ if (m_ESPs[m_ESPsByPriority.at(priority)].forceEnabled ||
std::find_if(m_LockedOrder.begin(), m_LockedOrder.end(), [&](const std::pair<QString, int> &a) { return a.second == priority; }) != m_LockedOrder.end()) {
// Attempt to find a priority but step over force-enabled plugins and already-set locks
int calcPriority = priority;
do {
++calcPriority;
- } while (calcPriority < m_ESPsByPriority.size() || (m_ESPs[m_ESPsByPriority.at(calcPriority)].m_ForceEnabled &&
+ } while (calcPriority < m_ESPsByPriority.size() || (m_ESPs[m_ESPsByPriority.at(calcPriority)].forceEnabled &&
std::find_if(m_LockedOrder.begin(), m_LockedOrder.end(), [&](const std::pair<QString, int> &a) { return a.second == calcPriority; }) != m_LockedOrder.end()));
// If we have a match, we can reassign the priority...
if (calcPriority < m_ESPsByPriority.size())
@@ -463,6 +488,7 @@ void PluginList::readLockedOrderFrom(const QString &fileName)
}
}
} else {
+ log::error("locked order file: invalid line #{} '{}'", lineNumber, QString::fromUtf8(line));
reportError(tr("The file containing locked plugin indices is broken"));
break;
}
@@ -481,7 +507,7 @@ void PluginList::writeLockedOrder(const QString &fileName) const
file->write(QString("%1|%2\r\n").arg(iter->first).arg(iter->second).toUtf8());
}
file.commit();
- qDebug("%s saved", qUtf8Printable(QDir::toNativeSeparators(fileName)));
+ log::debug("{} saved", QDir::toNativeSeparators(fileName));
}
@@ -500,13 +526,13 @@ void PluginList::saveTo(const QString &lockedOrderFileName
for (size_t i = 0; i < m_ESPs.size(); ++i) {
int priority = m_ESPsByPriority[i];
- if (!m_ESPs[priority].m_Enabled) {
- deleterFile->write(m_ESPs[priority].m_Name.toUtf8());
+ if (!m_ESPs[priority].enabled) {
+ deleterFile->write(m_ESPs[priority].name.toUtf8());
deleterFile->write("\r\n");
}
}
if (deleterFile.commitIfDifferent(m_LastSaveHash[deleterFileName])) {
- qDebug("%s saved", qUtf8Printable(QDir::toNativeSeparators(deleterFileName)));
+ log::debug("{} saved", QDir::toNativeSeparators(deleterFileName));
}
} else if (QFile::exists(deleterFileName)) {
shellDelete(QStringList() << deleterFileName);
@@ -521,16 +547,19 @@ bool PluginList::saveLoadOrder(DirectoryEntry &directoryStructure)
return true;
}
- qDebug("setting file times on esps");
+ log::debug("setting file times on esps");
for (ESPInfo &esp : m_ESPs) {
- std::wstring espName = ToWString(esp.m_Name);
+ std::wstring espName = ToWString(esp.name);
const FileEntry::Ptr fileEntry = directoryStructure.findFile(espName);
if (fileEntry.get() != nullptr) {
QString fileName;
bool archive = false;
int originid = fileEntry->getOrigin(archive);
- fileName = QString("%1\\%2").arg(QDir::toNativeSeparators(ToQString(directoryStructure.getOriginByID(originid).getPath()))).arg(esp.m_Name);
+
+ fileName = QString("%1\\%2")
+ .arg(QDir::toNativeSeparators(ToQString(directoryStructure.getOriginByID(originid).getPath())))
+ .arg(esp.name);
HANDLE file = ::CreateFile(ToWString(fileName).c_str(), GENERIC_READ | GENERIC_WRITE,
0, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
@@ -544,13 +573,13 @@ bool PluginList::saveLoadOrder(DirectoryEntry &directoryStructure)
}
ULONGLONG temp = 0;
- temp = (145731ULL + esp.m_Priority) * 24 * 60 * 60 * 10000000ULL;
+ temp = (145731ULL + esp.priority) * 24 * 60 * 60 * 10000000ULL;
FILETIME newWriteTime;
newWriteTime.dwLowDateTime = (DWORD)(temp & 0xFFFFFFFF);
newWriteTime.dwHighDateTime = (DWORD)(temp >> 32);
- esp.m_Time = newWriteTime;
+ esp.time = newWriteTime;
fileEntry->setFileTime(newWriteTime);
if (!::SetFileTime(file, nullptr, nullptr, &newWriteTime)) {
throw windows_error(QObject::tr("failed to set file time %1").arg(fileName).toUtf8().constData());
@@ -566,7 +595,7 @@ int PluginList::enabledCount() const
{
int enabled = 0;
for (const auto &info : m_ESPs) {
- if (info.m_Enabled) {
+ if (info.enabled) {
++enabled;
}
}
@@ -575,19 +604,19 @@ int PluginList::enabledCount() const
QString PluginList::getIndexPriority(int index) const
{
- return m_ESPs[index].m_Index;
+ return m_ESPs[index].index;
}
bool PluginList::isESPLocked(int index) const
{
- return m_LockedOrder.find(m_ESPs.at(index).m_Name.toLower()) != m_LockedOrder.end();
+ return m_LockedOrder.find(m_ESPs.at(index).name.toLower()) != m_LockedOrder.end();
}
void PluginList::lockESPIndex(int index, bool lock)
{
if (lock) {
- if (!m_ESPs.at(index).m_ForceEnabled)
- m_LockedOrder[getName(index).toLower()] = m_ESPs.at(index).m_LoadOrder;
+ if (!m_ESPs.at(index).forceEnabled)
+ m_LockedOrder[getName(index).toLower()] = m_ESPs.at(index).loadOrder;
else
return;
} else {
@@ -605,10 +634,10 @@ void PluginList::syncLoadOrder()
for (unsigned int i = 0; i < m_ESPs.size(); ++i) {
int index = m_ESPsByPriority[i];
- if (m_ESPs[index].m_Enabled) {
- m_ESPs[index].m_LoadOrder = loadOrder++;
+ if (m_ESPs[index].enabled) {
+ m_ESPs[index].loadOrder = loadOrder++;
} else {
- m_ESPs[index].m_LoadOrder = -1;
+ m_ESPs[index].loadOrder = -1;
}
}
}
@@ -633,7 +662,7 @@ void PluginList::refreshLoadOrder()
// find the location to insert at
while ((targetPrio < static_cast<int>(m_ESPs.size() - 1)) &&
- (m_ESPs[m_ESPsByPriority[targetPrio]].m_LoadOrder < iter->first)) {
+ (m_ESPs[m_ESPsByPriority[targetPrio]].loadOrder < iter->first)) {
++targetPrio;
}
@@ -643,9 +672,9 @@ void PluginList::refreshLoadOrder()
int temp = targetPrio;
int index = nameIter->second;
- if (m_ESPs[index].m_Priority != temp) {
+ if (m_ESPs[index].priority != temp) {
setPluginPriority(index, temp);
- m_ESPs[index].m_LoadOrder = iter->first;
+ m_ESPs[index].loadOrder = iter->first;
syncLoadOrder();
savePluginsList = true;
}
@@ -672,7 +701,7 @@ QStringList PluginList::pluginNames() const
QStringList result;
for (const ESPInfo &info : m_ESPs) {
- result.append(info.m_Name);
+ result.append(info.name);
}
return result;
@@ -684,37 +713,37 @@ IPluginList::PluginStates PluginList::state(const QString &name) const
if (iter == m_ESPsByName.end()) {
return IPluginList::STATE_MISSING;
} else {
- return m_ESPs[iter->second].m_Enabled ? IPluginList::STATE_ACTIVE : IPluginList::STATE_INACTIVE;
+ return m_ESPs[iter->second].enabled ? IPluginList::STATE_ACTIVE : IPluginList::STATE_INACTIVE;
}
}
void PluginList::setState(const QString &name, PluginStates state) {
auto iter = m_ESPsByName.find(name.toLower());
if (iter != m_ESPsByName.end()) {
- m_ESPs[iter->second].m_Enabled = (state == IPluginList::STATE_ACTIVE) ||
- m_ESPs[iter->second].m_ForceEnabled;
+ m_ESPs[iter->second].enabled = (state == IPluginList::STATE_ACTIVE) ||
+ m_ESPs[iter->second].forceEnabled;
} else {
- qWarning("Plugin not found: %s", qUtf8Printable(name));
+ log::warn("Plugin not found: {}", name);
}
}
void PluginList::setLoadOrder(const QStringList &pluginList)
{
for (ESPInfo &info : m_ESPs) {
- info.m_Priority = -1;
+ info.priority = -1;
}
int maxPriority = 0;
for (const QString &plugin : pluginList) {
auto iter = m_ESPsByName.find(plugin.toLower());
if (iter !=m_ESPsByName.end()) {
- m_ESPs[iter->second].m_Priority = maxPriority++;
+ m_ESPs[iter->second].priority = maxPriority++;
}
}
// use old priorities
for (ESPInfo &info : m_ESPs) {
- if (info.m_Priority == -1) {
- info.m_Priority = maxPriority++;
+ if (info.priority == -1) {
+ info.priority = maxPriority++;
}
}
updateIndices();
@@ -726,7 +755,7 @@ int PluginList::priority(const QString &name) const
if (iter == m_ESPsByName.end()) {
return -1;
} else {
- return m_ESPs[iter->second].m_Priority;
+ return m_ESPs[iter->second].priority;
}
}
@@ -736,7 +765,7 @@ int PluginList::loadOrder(const QString &name) const
if (iter == m_ESPsByName.end()) {
return -1;
} else {
- return m_ESPs[iter->second].m_LoadOrder;
+ return m_ESPs[iter->second].loadOrder;
}
}
@@ -746,7 +775,7 @@ bool PluginList::isMaster(const QString &name) const
if (iter == m_ESPsByName.end()) {
return false;
} else {
- return m_ESPs[iter->second].m_IsMaster;
+ return m_ESPs[iter->second].isMaster;
}
}
@@ -756,7 +785,7 @@ bool PluginList::isLight(const QString &name) const
if (iter == m_ESPsByName.end()) {
return false;
} else {
- return m_ESPs[iter->second].m_IsLight;
+ return m_ESPs[iter->second].isLight;
}
}
@@ -766,7 +795,7 @@ bool PluginList::isLightFlagged(const QString &name) const
if (iter == m_ESPsByName.end()) {
return false;
} else {
- return m_ESPs[iter->second].m_IsLightFlagged;
+ return m_ESPs[iter->second].isLightFlagged;
}
}
@@ -777,7 +806,7 @@ QStringList PluginList::masters(const QString &name) const
return QStringList();
} else {
QStringList result;
- for (const QString &master : m_ESPs[iter->second].m_Masters) {
+ for (const QString &master : m_ESPs[iter->second].masters) {
result.append(master);
}
return result;
@@ -790,7 +819,7 @@ QString PluginList::origin(const QString &name) const
if (iter == m_ESPsByName.end()) {
return QString();
} else {
- return m_ESPs[iter->second].m_OriginName;
+ return m_ESPs[iter->second].originName;
}
}
@@ -820,15 +849,15 @@ void PluginList::updateIndices()
m_ESPsByPriority.clear();
m_ESPsByPriority.resize(m_ESPs.size());
for (unsigned int i = 0; i < m_ESPs.size(); ++i) {
- if (m_ESPs[i].m_Priority < 0) {
+ if (m_ESPs[i].priority < 0) {
continue;
}
- if (m_ESPs[i].m_Priority >= static_cast<int>(m_ESPs.size())) {
- qCritical("invalid plugin priority: %d", m_ESPs[i].m_Priority);
+ if (m_ESPs[i].priority >= static_cast<int>(m_ESPs.size())) {
+ log::error("invalid plugin priority: {}", m_ESPs[i].priority);
continue;
}
- m_ESPsByName[m_ESPs[i].m_Name.toLower()] = i;
- m_ESPsByPriority.at(static_cast<size_t>(m_ESPs[i].m_Priority)) = i;
+ m_ESPsByName[m_ESPs[i].name.toLower()] = i;
+ m_ESPsByPriority.at(static_cast<size_t>(m_ESPs[i].priority)) = i;
}
generatePluginIndexes();
@@ -838,19 +867,20 @@ void PluginList::generatePluginIndexes()
{
int numESLs = 0;
int numSkipped = 0;
+ bool lightPluginsSupported = m_GamePlugin->feature<GamePlugins>()->lightPluginsAreSupported();
for (int l = 0; l < m_ESPs.size(); ++l) {
int i = m_ESPsByPriority.at(l);
- if (!m_ESPs[i].m_Enabled) {
- m_ESPs[i].m_Index = QString();
+ if (!m_ESPs[i].enabled) {
+ m_ESPs[i].index = QString();
++numSkipped;
continue;
}
- if (m_ESPs[i].m_IsLight || m_ESPs[i].m_IsLightFlagged) {
+ if (lightPluginsSupported && (m_ESPs[i].isLight || m_ESPs[i].isLightFlagged)) {
int ESLpos = 254 + ((numESLs + 1) / 4096);
- m_ESPs[i].m_Index = QString("%1:%2").arg(ESLpos, 2, 16, QChar('0')).arg((numESLs) % 4096, 3, 16, QChar('0')).toUpper();
+ m_ESPs[i].index = QString("%1:%2").arg(ESLpos, 2, 16, QChar('0')).arg((numESLs) % 4096, 3, 16, QChar('0')).toUpper();
++numESLs;
} else {
- m_ESPs[i].m_Index = QString("%1").arg(l - numESLs - numSkipped, 2, 16, QChar('0')).toUpper();
+ m_ESPs[i].index = QString("%1").arg(l - numESLs - numSkipped, 2, 16, QChar('0')).toUpper();
}
}
emit esplist_changed();
@@ -874,160 +904,360 @@ int PluginList::columnCount(const QModelIndex &) const
void PluginList::testMasters()
{
-// emit layoutAboutToBeChanged();
-
std::set<QString> enabledMasters;
for (const auto& iter: m_ESPs) {
- if (iter.m_Enabled) {
- enabledMasters.insert(iter.m_Name.toLower());
+ if (iter.enabled) {
+ enabledMasters.insert(iter.name.toLower());
}
}
for (auto& iter: m_ESPs) {
- iter.m_MasterUnset.clear();
- if (iter.m_Enabled) {
- for (const auto& master: iter.m_Masters) {
+ iter.masterUnset.clear();
+ if (iter.enabled) {
+ for (const auto& master: iter.masters) {
if (enabledMasters.find(master.toLower()) == enabledMasters.end()) {
- iter.m_MasterUnset.insert(master);
+ iter.masterUnset.insert(master);
}
}
}
}
-
-#pragma message("emitting this seems to cause a crash!")
-// emit layoutChanged();
}
QVariant PluginList::data(const QModelIndex &modelIndex, int role) const
{
int index = modelIndex.row();
- if ((role == Qt::DisplayRole)
- || (role == Qt::EditRole)) {
- switch (modelIndex.column()) {
- case COL_NAME: {
- return m_ESPs[index].m_Name;
- } break;
- case COL_PRIORITY: {
- return m_ESPs[index].m_Priority;
- } break;
- case COL_MODINDEX: {
- return m_ESPs[index].m_Index;
- } break;
- default: {
- return QVariant();
- } break;
- }
+
+ if ((role == Qt::DisplayRole) || (role == Qt::EditRole)) {
+ return displayData(modelIndex);
} else if ((role == Qt::CheckStateRole) && (modelIndex.column() == 0)) {
- if (m_ESPs[index].m_ForceEnabled) {
- return QVariant();
- } else {
- return m_ESPs[index].m_Enabled ? Qt::Checked : Qt::Unchecked;
- }
+ return checkstateData(modelIndex);
} else if (role == Qt::ForegroundRole) {
- if ((modelIndex.column() == COL_NAME) &&
- m_ESPs[index].m_ForceEnabled) {
- return QBrush(Qt::gray);
- }
- } else if (role == Qt::BackgroundRole
- || (role == ViewMarkingScrollBar::DEFAULT_ROLE)) {
- if (m_ESPs[index].m_ModSelected) {
- return Settings::instance().pluginListContainedColor();
- } else {
- return QVariant();
- }
+ return foregroundData(modelIndex);
+ } else if (role == Qt::BackgroundRole || (role == ViewMarkingScrollBar::DEFAULT_ROLE)) {
+ return backgroundData(modelIndex);
} else if (role == Qt::FontRole) {
- QFont result;
- if (m_ESPs[index].m_IsMaster) {
- result.setItalic(true);
- result.setWeight(QFont::Bold);
- } else if (m_ESPs[index].m_IsLight || m_ESPs[index].m_IsLightFlagged) {
- result.setItalic(true);
- }
- return result;
+ return fontData(modelIndex);
} else if (role == Qt::TextAlignmentRole) {
- if (modelIndex.column() == 0) {
- return QVariant(Qt::AlignLeft | Qt::AlignVCenter);
- } else {
- return QVariant(Qt::AlignHCenter | Qt::AlignVCenter);
- }
+ return alignmentData(modelIndex);
} else if (role == Qt::ToolTipRole) {
- QString name = m_ESPs[index].m_Name.toLower();
- auto addInfoIter = m_AdditionalInfo.find(name);
- QString toolTip;
- if (addInfoIter != m_AdditionalInfo.end()) {
- if (!addInfoIter->second.m_Messages.isEmpty()) {
- toolTip += addInfoIter->second.m_Messages.join("<br>") + "<br><hr>";
- }
+ return tooltipData(modelIndex);
+ } else if (role == Qt::UserRole + 1) {
+ return iconData(modelIndex);
+ }
+ return QVariant();
+}
+
+QVariant PluginList::displayData(const QModelIndex &modelIndex) const
+{
+ const int index = modelIndex.row();
+
+ switch (modelIndex.column())
+ {
+ case COL_NAME:
+ return m_ESPs[index].name;
+
+ case COL_PRIORITY:
+ return m_ESPs[index].priority;
+
+ case COL_MODINDEX:
+ return m_ESPs[index].index;
+
+ default:
+ return {};
+ }
+}
+
+QVariant PluginList::checkstateData(const QModelIndex &modelIndex) const
+{
+ const int index = modelIndex.row();
+
+ if (m_ESPs[index].forceEnabled) {
+ return {};
+ }
+
+ return m_ESPs[index].enabled ? Qt::Checked : Qt::Unchecked;
+}
+
+QVariant PluginList::foregroundData(const QModelIndex &modelIndex) const
+{
+ const int index = modelIndex.row();
+
+ if ((modelIndex.column() == COL_NAME) && m_ESPs[index].forceEnabled) {
+ return QBrush(Qt::gray);
+ }
+
+ return {};
+}
+
+QVariant PluginList::backgroundData(const QModelIndex &modelIndex) const
+{
+ const int index = modelIndex.row();
+
+ if (m_ESPs[index].modSelected) {
+ return Settings::instance().colors().pluginListContained();
+ }
+
+ return {};
+}
+
+QVariant PluginList::fontData(const QModelIndex &modelIndex) const
+{
+ const int index = modelIndex.row();
+
+ QFont result;
+
+ if (m_ESPs[index].isMaster) {
+ result.setItalic(true);
+ result.setWeight(QFont::Bold);
+ } else if (m_ESPs[index].isLight || m_ESPs[index].isLightFlagged) {
+ result.setItalic(true);
+ }
+
+ return result;
+}
+
+QVariant PluginList::alignmentData(const QModelIndex &modelIndex) const
+{
+ const int index = modelIndex.row();
+
+ if (modelIndex.column() == 0) {
+ return QVariant(Qt::AlignLeft | Qt::AlignVCenter);
+ } else {
+ return QVariant(Qt::AlignHCenter | Qt::AlignVCenter);
+ }
+}
+
+QVariant PluginList::tooltipData(const QModelIndex &modelIndex) const
+{
+ const int index = modelIndex.row();
+ const auto& esp = m_ESPs[index];
+
+ QString toolTip;
+
+ toolTip += "<b>" + tr("Origin") + "</b>: " + esp.originName;
+
+ if (esp.forceEnabled) {
+ toolTip +=
+ "<br><b><i>" +
+ tr("This plugin can't be disabled (enforced by the game).") +
+ "</i></b>";
+ } else {
+ if (!esp.author.isEmpty()) {
+ toolTip +=
+ "<br><b>" + tr("Author") + "</b>: " +
+ TruncateString(esp.author);
}
- if (m_ESPs[index].m_ForceEnabled) {
- QString text = tr("<b>Origin</b>: %1").arg(m_ESPs[index].m_OriginName);
- text += tr("<br><b><i>This plugin can't be disabled (enforced by the game).</i></b>");
- toolTip += text;
- } else {
- QString text = tr("<b>Origin</b>: %1").arg(m_ESPs[index].m_OriginName);
- if (m_ESPs[index].m_Author.size() > 0) {
- text += "<br><b>" + tr("Author") + "</b>: " + TruncateString(m_ESPs[index].m_Author);
- }
- if (m_ESPs[index].m_Description.size() > 0) {
- text += "<br><b>" + tr("Description") + "</b>: " + TruncateString(m_ESPs[index].m_Description);
- }
- if (m_ESPs[index].m_MasterUnset.size() > 0) {
- text += "<br><b>" + tr("Missing Masters") + "</b>: <b>" + TruncateString(SetJoin(m_ESPs[index].m_MasterUnset, ", ")) + "</b>";
- }
- std::set<QString> enabledMasters;
- std::set_difference(m_ESPs[index].m_Masters.begin(), m_ESPs[index].m_Masters.end(),
- m_ESPs[index].m_MasterUnset.begin(), m_ESPs[index].m_MasterUnset.end(),
- std::inserter(enabledMasters, enabledMasters.end()));
- if (!enabledMasters.empty()) {
- text += "<br><b>" + tr("Enabled Masters") + "</b>: " + TruncateString(SetJoin(enabledMasters, ", "));
- }
- if (!m_ESPs[index].m_Archives.empty()) {
- text += "<br><b>" + tr("Loads Archives") + "</b>: " + TruncateString(SetJoin(m_ESPs[index].m_Archives, ", "));
- text += "<br>" + tr("There are Archives connected to this plugin. "
- "Their assets will be added to your game, overwriting in case of conflicts following the plugin order. "
- "Loose files will always overwrite assets from Archives. (This flag only checks for Archives from the same mod as the plugin)");
- }
- if (m_ESPs[index].m_HasIni) {
- text += "<br><b>" + tr("Loads INI settings") + "</b>: ";
- text += "<br>" + tr("There is an ini file connected to this plugin. "
- "Its settings will be added to your game settings, overwriting in case of conflicts.");
- }
- if (m_ESPs[index].m_IsLightFlagged && !m_ESPs[index].m_IsLight) {
- text += "<br><br>" + tr("This ESP is flagged as an ESL. "
- "It will adhere to the ESP load order but the records will be loaded in ESL space.");
- }
- toolTip += text;
+
+ if (esp.description.size() > 0) {
+ toolTip +=
+ "<br><b>" + tr("Description") + "</b>: " +
+ TruncateString(esp.description);
}
- return toolTip;
- } else if (role == Qt::UserRole + 1) {
- QVariantList result;
- QString nameLower = m_ESPs[index].m_Name.toLower();
- if (m_ESPs[index].m_MasterUnset.size() > 0) {
- result.append(":/MO/gui/warning");
+
+ if (esp.masterUnset.size() > 0) {
+ toolTip +=
+ "<br><b>" + tr("Missing Masters") + "</b>: " +
+ "<b>" + TruncateString(SetJoin(esp.masterUnset, ", ")) + "</b>";
+ }
+
+ std::set<QString> enabledMasters;
+ std::set_difference(esp.masters.begin(), esp.masters.end(),
+ esp.masterUnset.begin(), esp.masterUnset.end(),
+ std::inserter(enabledMasters, enabledMasters.end()));
+
+ if (!enabledMasters.empty()) {
+ toolTip +=
+ "<br><b>" + tr("Enabled Masters") + "</b>: " +
+ TruncateString(SetJoin(enabledMasters, ", "));
+ }
+
+ if (!esp.archives.empty()) {
+ toolTip +=
+ "<br><b>" + tr("Loads Archives") + "</b>: " +
+ TruncateString(SetJoin(esp.archives, ", ")) +
+ "<br>" + tr(
+ "There are Archives connected to this plugin. Their assets will be "
+ "added to your game, overwriting in case of conflicts following the "
+ "plugin order. Loose files will always overwrite assets from "
+ "Archives. (This flag only checks for Archives from the same mod as "
+ "the plugin)");
+ }
+
+ if (esp.hasIni) {
+ toolTip +=
+ "<br><b>" + tr("Loads INI settings") + "</b>: "
+ "<br>" + tr(
+ "There is an ini file connected to this plugin. Its settings will "
+ "be added to your game settings, overwriting in case of conflicts.");
}
- if (m_LockedOrder.find(nameLower) != m_LockedOrder.end()) {
- result.append(":/MO/gui/locked");
+
+ if (esp.isLightFlagged && !esp.isLight) {
+ toolTip +=
+ "<br><br>" + tr(
+ "This ESP is flagged as an ESL. It will adhere to the ESP load "
+ "order but the records will be loaded in ESL space.");
}
- auto bossInfoIter = m_AdditionalInfo.find(nameLower);
- if (bossInfoIter != m_AdditionalInfo.end()) {
- if (!bossInfoIter->second.m_Messages.isEmpty()) {
- result.append(":/MO/gui/information");
+ }
+
+
+ // additional info
+ auto itor = m_AdditionalInfo.find(esp.name.toLower());
+
+ if (itor != m_AdditionalInfo.end()) {
+ if (!itor->second.messages.isEmpty()) {
+ toolTip += "<hr><ul style=\"margin-left:15px; -qt-list-indent: 0;\">";
+
+ for (auto&& message : itor->second.messages) {
+ toolTip += "<li>" + message + "</li>";
}
+
+ toolTip += "</ul>";
}
- if (m_ESPs[index].m_HasIni) {
- result.append(":/MO/gui/attachment");
+
+ // loot
+ toolTip += makeLootTooltip(itor->second.loot);
+ }
+
+ return toolTip;
+}
+
+QString PluginList::makeLootTooltip(const Loot::Plugin& loot) const
+{
+ QString s;
+
+ for (auto&& f : loot.incompatibilities) {
+ s +=
+ "<li>" + tr("Incompatible with %1")
+ .arg(f.displayName.isEmpty() ? f.name : f.displayName) +
+ "</li>";
+ }
+
+ for (auto&& m : loot.missingMasters) {
+ s += "<li>" + tr("Depends on missing %1").arg(m) + "</li>";
+ }
+
+ for (auto&& m : loot.messages) {
+ s += "<li>";
+
+ switch (m.type)
+ {
+ case log::Warning:
+ s += tr("Warning") + ": ";
+ break;
+
+ case log::Error:
+ s += tr("Error") + ": ";
+ break;
+
+ case log::Info: // fall-through
+ case log::Debug:
+ default:
+ // nothing
+ break;
}
- if (!m_ESPs[index].m_Archives.empty()) {
- result.append(":/MO/gui/archive_conflict_neutral");
+
+ s += m.text + "</li>";
+ }
+
+ for (auto&& d : loot.dirty) {
+ s += "<li>" + d.toString(false) + "</li>";
+ }
+
+ for (auto&& c : loot.clean) {
+ s += "<li>" + c.toString(true) + "</li>";
+ }
+
+ if (!s.isEmpty()) {
+ s =
+ "<hr>"
+ "<ul style=\"margin-top:0px; padding-top:0px; margin-left:15px; -qt-list-indent: 0;\">" +
+ s +
+ "</ul>";
+ }
+
+ return s;
+}
+
+QVariant PluginList::iconData(const QModelIndex &modelIndex) const
+{
+ int index = modelIndex.row();
+
+ QVariantList result;
+
+ const auto& esp = m_ESPs[index];
+ const QString nameLower = esp.name.toLower();
+
+ auto infoItor = m_AdditionalInfo.find(nameLower);
+
+ const AdditionalInfo* info = nullptr;
+ if (infoItor != m_AdditionalInfo.end()) {
+ info = &infoItor->second;
+ }
+
+ if (isProblematic(esp, info)) {
+ result.append(":/MO/gui/warning");
+ }
+
+ if (m_LockedOrder.find(nameLower) != m_LockedOrder.end()) {
+ result.append(":/MO/gui/locked");
+ }
+
+ if (hasInfo(esp, info)) {
+ result.append(":/MO/gui/information");
+ }
+
+ if (esp.hasIni) {
+ result.append(":/MO/gui/attachment");
+ }
+
+ if (!esp.archives.empty()) {
+ result.append(":/MO/gui/archive_conflict_neutral");
+ }
+
+ if (esp.isLightFlagged && !esp.isLight) {
+ result.append(":/MO/gui/awaiting");
+ }
+
+ if (info && !info->loot.dirty.empty()) {
+ result.append(":/MO/gui/edit_clear");
+ }
+
+ return result;
+}
+
+bool PluginList::isProblematic(const ESPInfo& esp, const AdditionalInfo* info) const
+{
+ if (esp.masterUnset.size() > 0) {
+ return true;
+ }
+
+ if (info) {
+ if (!info->loot.incompatibilities.empty()) {
+ return true;
}
- if (m_ESPs[index].m_IsLightFlagged && !m_ESPs[index].m_IsLight) {
- result.append(":/MO/gui/awaiting");
+
+ if (!info->loot.missingMasters.empty()) {
+ return true;
}
- return result;
}
- return QVariant();
+
+ return false;
}
+bool PluginList::hasInfo(const ESPInfo& esp, const AdditionalInfo* info) const
+{
+ if (info) {
+ if (!info->messages.empty()) {
+ return true;
+ }
+
+ if (!info->loot.messages.empty()) {
+ return true;
+ }
+ }
+
+ return false;
+}
bool PluginList::setData(const QModelIndex &modIndex, const QVariant &value, int role)
{
@@ -1037,8 +1267,8 @@ bool PluginList::setData(const QModelIndex &modIndex, const QVariant &value, int
bool result = false;
if (role == Qt::CheckStateRole) {
- m_ESPs[modIndex.row()].m_Enabled =
- value.toInt() == Qt::Checked || m_ESPs[modIndex.row()].m_ForceEnabled;
+ m_ESPs[modIndex.row()].enabled =
+ value.toInt() == Qt::Checked || m_ESPs[modIndex.row()].forceEnabled;
m_LastCheck.restart();
emit dataChanged(modIndex, modIndex);
@@ -1067,9 +1297,9 @@ bool PluginList::setData(const QModelIndex &modIndex, const QVariant &value, int
this->index(0, 0),
this->index(static_cast<int>(m_ESPs.size()), columnCount()));
} catch (const std::exception &e) {
- qCritical("failed to invoke state changed notification: %s", e.what());
+ log::error("failed to invoke state changed notification: {}", e.what());
} catch (...) {
- qCritical("failed to invoke state changed notification: unknown exception");
+ log::error("failed to invoke state changed notification: unknown exception");
}
}
@@ -1102,7 +1332,7 @@ Qt::ItemFlags PluginList::flags(const QModelIndex &modelIndex) const
Qt::ItemFlags result = QAbstractItemModel::flags(modelIndex);
if (modelIndex.isValid()) {
- if (!m_ESPs[index].m_ForceEnabled) {
+ if (!m_ESPs[index].forceEnabled) {
result |= Qt::ItemIsUserCheckable | Qt::ItemIsDragEnabled;
}
if (modelIndex.column() == COL_PRIORITY) {
@@ -1127,48 +1357,48 @@ void PluginList::setPluginPriority(int row, int &newPriority)
else if (newPriorityTemp >= static_cast<int>(m_ESPsByPriority.size()))
newPriorityTemp = static_cast<int>(m_ESPsByPriority.size()) - 1;
- if (!m_ESPs[row].m_IsMaster && !m_ESPs[row].m_IsLight) {
+ if (!m_ESPs[row].isMaster && !m_ESPs[row].isLight) {
// don't allow esps to be moved above esms
while ((newPriorityTemp < static_cast<int>(m_ESPsByPriority.size() - 1)) &&
- (m_ESPs.at(m_ESPsByPriority.at(newPriorityTemp)).m_IsMaster ||
- m_ESPs.at(m_ESPsByPriority.at(newPriorityTemp)).m_IsLight)) {
+ (m_ESPs.at(m_ESPsByPriority.at(newPriorityTemp)).isMaster ||
+ m_ESPs.at(m_ESPsByPriority.at(newPriorityTemp)).isLight)) {
++newPriorityTemp;
}
} else {
// don't allow esms to be moved below esps
while ((newPriorityTemp > 0) &&
- !m_ESPs.at(m_ESPsByPriority.at(newPriorityTemp)).m_IsMaster &&
- !m_ESPs.at(m_ESPsByPriority.at(newPriorityTemp)).m_IsLight) {
+ !m_ESPs.at(m_ESPsByPriority.at(newPriorityTemp)).isMaster &&
+ !m_ESPs.at(m_ESPsByPriority.at(newPriorityTemp)).isLight) {
--newPriorityTemp;
}
// also don't allow "regular" esms to be moved above primary plugins
while ((newPriorityTemp < static_cast<int>(m_ESPsByPriority.size() - 1)) &&
- (m_ESPs.at(m_ESPsByPriority.at(newPriorityTemp)).m_ForceEnabled)) {
+ (m_ESPs.at(m_ESPsByPriority.at(newPriorityTemp)).forceEnabled)) {
++newPriorityTemp;
}
}
try {
- int oldPriority = m_ESPs.at(row).m_Priority;
+ int oldPriority = m_ESPs.at(row).priority;
if (newPriorityTemp > oldPriority) {
// priority is higher than the old, so the gap we left is in lower priorities
for (int i = oldPriority + 1; i <= newPriorityTemp; ++i) {
- --m_ESPs.at(m_ESPsByPriority.at(i)).m_Priority;
+ --m_ESPs.at(m_ESPsByPriority.at(i)).priority;
}
emit dataChanged(index(oldPriority + 1, 0), index(newPriorityTemp, columnCount()));
} else {
for (int i = newPriorityTemp; i < oldPriority; ++i) {
- ++m_ESPs.at(m_ESPsByPriority.at(i)).m_Priority;
+ ++m_ESPs.at(m_ESPsByPriority.at(i)).priority;
}
emit dataChanged(index(newPriorityTemp, 0), index(oldPriority - 1, columnCount()));
++newPriority;
}
- m_ESPs.at(row).m_Priority = newPriorityTemp;
+ m_ESPs.at(row).priority = newPriorityTemp;
emit dataChanged(index(row, 0), index(row, columnCount()));
- m_PluginMoved(m_ESPs[row].m_Name, oldPriority, newPriorityTemp);
+ m_PluginMoved(m_ESPs[row].name, oldPriority, newPriorityTemp);
} catch (const std::out_of_range&) {
- reportError(tr("failed to restore load order for %1").arg(m_ESPs[row].m_Name));
+ reportError(tr("failed to restore load order for %1").arg(m_ESPs[row].name));
}
updateIndices();
@@ -1186,11 +1416,11 @@ void PluginList::changePluginPriority(std::vector<int> rows, int newPriority)
// don't try to move plugins before force-enabled plugins
for (std::vector<ESPInfo>::const_iterator iter = m_ESPs.begin();
iter != m_ESPs.end(); ++iter) {
- if (iter->m_ForceEnabled) {
- newPriority = std::max(newPriority, iter->m_Priority+1);
+ if (iter->forceEnabled) {
+ newPriority = std::max(newPriority, iter->priority+1);
}
- maxPriority = std::max(maxPriority, iter->m_Priority+1);
- minPriority = std::min(minPriority, iter->m_Priority);
+ maxPriority = std::max(maxPriority, iter->priority+1);
+ minPriority = std::min(minPriority, iter->priority);
}
// limit the new priority to existing priorities
@@ -1200,14 +1430,14 @@ void PluginList::changePluginPriority(std::vector<int> rows, int newPriority)
// sort the moving plugins by ascending priorities
std::sort(rows.begin(), rows.end(),
[&esp](const int &LHS, const int &RHS) {
- return esp[LHS].m_Priority < esp[RHS].m_Priority;
+ return esp[LHS].priority < esp[RHS].priority;
});
// if at least on plugin is increasing in priority, the target index is
// that of the row BELOW the dropped location, otherwise it's the one above
for (std::vector<int>::const_iterator iter = rows.begin();
iter != rows.end(); ++iter) {
- if (m_ESPs[*iter].m_Priority < newPriority) {
+ if (m_ESPs[*iter].priority < newPriority) {
--newPriority;
break;
}
@@ -1252,7 +1482,7 @@ bool PluginList::dropMimeData(const QMimeData *mimeData, Qt::DropAction action,
(row >= static_cast<int>(m_ESPs.size()))) {
newPriority = static_cast<int>(m_ESPs.size());
} else {
- newPriority = m_ESPs[row].m_Priority;
+ newPriority = m_ESPs[row].priority;
}
changePluginPriority(sourceRows, newPriority);
@@ -1304,12 +1534,12 @@ bool PluginList::eventFilter(QObject *obj, QEvent *event)
}
if (keyEvent->key() == Qt::Key_Down) {
for (int i = 0; i < rows.size() / 2; ++i) {
- rows.swap(i, rows.size() - i - 1);
+ rows.swapItemsAt(i, rows.size() - i - 1);
}
}
for (QModelIndex idx : rows) {
idx = proxyModel->mapToSource(idx);
- int newPriority = m_ESPs[idx.row()].m_Priority + diff;
+ int newPriority = m_ESPs[idx.row()].priority + diff;
if ((newPriority >= 0) && (newPriority < rowCount())) {
setPluginPriority(idx.row(), newPriority);
}
@@ -1350,28 +1580,29 @@ bool PluginList::eventFilter(QObject *obj, QEvent *event)
PluginList::ESPInfo::ESPInfo(const QString &name, bool enabled,
const QString &originName, const QString &fullPath,
- bool hasIni, std::set<QString> archives)
- : m_Name(name), m_FullPath(fullPath), m_Enabled(enabled), m_ForceEnabled(enabled),
- m_Priority(0), m_LoadOrder(-1), m_OriginName(originName), m_HasIni(hasIni), m_Archives(archives), m_ModSelected(false)
+ bool hasIni, std::set<QString> archives, bool lightPluginsAreSupported)
+ : name(name), fullPath(fullPath), enabled(enabled), forceEnabled(enabled),
+ priority(0), loadOrder(-1), originName(originName), hasIni(hasIni),
+ archives(archives), modSelected(false)
{
try {
ESP::File file(ToWString(fullPath));
- m_IsMaster = file.isMaster();
+ isMaster = file.isMaster();
auto extension = name.right(3).toLower();
- m_IsLight = (extension == "esl");
- m_IsLightFlagged = file.isLight();
+ isLight = lightPluginsAreSupported && (extension == "esl");
+ isLightFlagged = lightPluginsAreSupported && file.isLight();
+
+ author = QString::fromLatin1(file.author().c_str());
+ description = QString::fromLatin1(file.description().c_str());
- m_Author = QString::fromLatin1(file.author().c_str());
- m_Description = QString::fromLatin1(file.description().c_str());
- std::set<std::string> masters = file.masters();
- for (auto iter = masters.begin(); iter != masters.end(); ++iter) {
- m_Masters.insert(QString(iter->c_str()));
+ for (auto&& m : file.masters()) {
+ masters.insert(QString::fromStdString(m));
}
} catch (const std::exception &e) {
- qCritical("failed to parse plugin file %s: %s", qUtf8Printable(fullPath), e.what());
- m_IsMaster = false;
- m_IsLight = false;
- m_IsLightFlagged = false;
+ log::error("failed to parse plugin file {}: {}", fullPath, e.what());
+ isMaster = false;
+ isLight = false;
+ isLightFlagged = false;
}
}
diff --git a/src/pluginlist.h b/src/pluginlist.h
index 228ccdec..004b1590 100644
--- a/src/pluginlist.h
+++ b/src/pluginlist.h
@@ -23,6 +23,8 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#include <directoryentry.h>
#include <ipluginlist.h>
#include "profile.h"
+#include "loot.h"
+
namespace MOBase { class IPluginGame; }
#include <QString>
@@ -155,6 +157,11 @@ public:
void addInformation(const QString &name, const QString &message);
/**
+ * adds information from a loot report
+ */
+ void addLootReport(const QString& name, Loot::Plugin plugin);
+
+ /**
* @brief test if a plugin is enabled
*
* @param index index of the plugin to look up
@@ -195,8 +202,8 @@ public:
int timeElapsedSinceLastChecked() const;
- QString getName(int index) const { return m_ESPs.at(index).m_Name; }
- int getPriority(int index) const { return m_ESPs.at(index).m_Priority; }
+ QString getName(int index) const { return m_ESPs.at(index).name; }
+ int getPriority(int index) const { return m_ESPs.at(index).priority; }
QString getIndexPriority(int index) const;
bool isESPLocked(int index) const;
void lockESPIndex(int index, bool lock);
@@ -294,36 +301,42 @@ signals:
private:
- struct ESPInfo {
+ struct ESPInfo
+ {
+ ESPInfo(
+ const QString &name, bool enabled, const QString &originName,
+ const QString &fullPath, bool hasIni, std::set<QString> archives,
+ bool lightSupported);
+
+ QString name;
+ QString fullPath;
+ bool enabled;
+ bool forceEnabled;
+ int priority;
+ QString index;
+ int loadOrder;
+ FILETIME time;
+ QString originName;
+ bool isMaster;
+ bool isLight;
+ bool isLightFlagged;
+ bool modSelected;
+ QString author;
+ QString description;
+ bool hasIni;
+ std::set<QString> archives;
+ std::set<QString> masters;
+ mutable std::set<QString> masterUnset;
- ESPInfo(const QString &name, bool enabled, const QString &originName, const QString &fullPath, bool hasIni, std::set<QString> archives);
- QString m_Name;
- QString m_FullPath;
- bool m_Enabled;
- bool m_ForceEnabled;
- int m_Priority;
- QString m_Index;
- int m_LoadOrder;
- FILETIME m_Time;
- QString m_OriginName;
- bool m_IsMaster;
- bool m_IsLight;
- bool m_IsLightFlagged;
- bool m_ModSelected;
- QString m_Author;
- QString m_Description;
- bool m_HasIni;
- std::set<QString> m_Archives;
- std::set<QString> m_Masters;
- mutable std::set<QString> m_MasterUnset;
bool operator < (const ESPInfo& str) const
{
- return (m_LoadOrder < str.m_LoadOrder);
+ return (loadOrder < str.loadOrder);
}
};
struct AdditionalInfo {
- QStringList m_Messages;
+ QStringList messages;
+ Loot::Plugin loot;
};
friend bool ByName(const ESPInfo& LHS, const ESPInfo& RHS);
@@ -372,6 +385,19 @@ private:
const MOBase::IPluginGame *m_GamePlugin;
+
+ QVariant displayData(const QModelIndex &modelIndex) const;
+ QVariant checkstateData(const QModelIndex &modelIndex) const;
+ QVariant foregroundData(const QModelIndex &modelIndex) const;
+ QVariant backgroundData(const QModelIndex &modelIndex) const;
+ QVariant fontData(const QModelIndex &modelIndex) const;
+ QVariant alignmentData(const QModelIndex &modelIndex) const;
+ QVariant tooltipData(const QModelIndex &modelIndex) const;
+ QVariant iconData(const QModelIndex &modelIndex) const;
+
+ QString makeLootTooltip(const Loot::Plugin& loot) const;
+ bool isProblematic(const ESPInfo& esp, const AdditionalInfo* info) const;
+ bool hasInfo(const ESPInfo& esp, const AdditionalInfo* info) const;
};
#pragma warning(pop)
diff --git a/src/previewdialog.cpp b/src/previewdialog.cpp
index de33cdd0..91a5f13e 100644
--- a/src/previewdialog.cpp
+++ b/src/previewdialog.cpp
@@ -1,5 +1,6 @@
#include "previewdialog.h"
#include "ui_previewdialog.h"
+#include "settings.h"
#include <QFileInfo>
PreviewDialog::PreviewDialog(const QString &fileName, QWidget *parent) :
@@ -17,6 +18,12 @@ PreviewDialog::~PreviewDialog()
delete ui;
}
+int PreviewDialog::exec()
+{
+ GeometrySaver gs(Settings::instance(), this);
+ return QDialog::exec();
+}
+
void PreviewDialog::addVariant(const QString &modName, QWidget *widget)
{
widget->setProperty("modName", modName);
diff --git a/src/previewdialog.h b/src/previewdialog.h
index 0011bc50..9525f127 100644
--- a/src/previewdialog.h
+++ b/src/previewdialog.h
@@ -15,6 +15,10 @@ public:
explicit PreviewDialog(const QString &fileName, QWidget *parent = 0);
~PreviewDialog();
+ // also saves and restores geometry
+ //
+ int exec() override;
+
void addVariant(const QString &modName, QWidget *widget);
int numVariants() const;
diff --git a/src/previewgenerator.cpp b/src/previewgenerator.cpp
index 280d69aa..f317393e 100644
--- a/src/previewgenerator.cpp
+++ b/src/previewgenerator.cpp
@@ -22,14 +22,11 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#include <QLabel>
#include <QImageReader>
#include <QTextEdit>
-#include <QDesktopWidget>
#include <utility.h>
PreviewGenerator::PreviewGenerator()
{
-
- QDesktopWidget desk;
- m_MaxSize = desk.screenGeometry().size() * 0.8;
+ m_MaxSize = QGuiApplication::primaryScreen()->size() * 0.8;
}
void PreviewGenerator::registerPlugin(MOBase::IPluginPreview *plugin)
diff --git a/src/problemsdialog.cpp b/src/problemsdialog.cpp
index 1e8e800f..ea23beec 100644
--- a/src/problemsdialog.cpp
+++ b/src/problemsdialog.cpp
@@ -29,6 +29,12 @@ ProblemsDialog::~ProblemsDialog()
delete ui;
}
+int ProblemsDialog::exec()
+{
+ GeometrySaver gs(Settings::instance(), this);
+ return QDialog::exec();
+}
+
void ProblemsDialog::runDiagnosis()
{
m_hasProblems = false;
@@ -96,7 +102,7 @@ void ProblemsDialog::startFix()
{
QObject *fixButton = QObject::sender();
if (fixButton == NULL) {
- qWarning("no button");
+ log::warn("no button");
return;
}
IPluginDiagnose *plugin = reinterpret_cast<IPluginDiagnose*>(fixButton ->property("fix").value<void*>());
@@ -106,5 +112,5 @@ void ProblemsDialog::startFix()
void ProblemsDialog::urlClicked(const QUrl &url)
{
- shell::OpenLink(url);
+ shell::Open(url);
}
diff --git a/src/problemsdialog.h b/src/problemsdialog.h
index c211e4f5..a30c8d48 100644
--- a/src/problemsdialog.h
+++ b/src/problemsdialog.h
@@ -20,6 +20,10 @@ public:
explicit ProblemsDialog(std::vector<QObject*> pluginObjects, QWidget *parent = 0);
~ProblemsDialog();
+ // also saves and restores geometry
+ //
+ int exec() override;
+
bool hasProblems() const;
private:
diff --git a/src/problemsdialog.ui b/src/problemsdialog.ui
index a1712d4a..a474f73b 100644
--- a/src/problemsdialog.ui
+++ b/src/problemsdialog.ui
@@ -50,7 +50,7 @@
&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;
p, li { white-space: pre-wrap; }
&lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:'MS Shell Dlg 2'; font-size:8.25pt; font-weight:400; font-style:normal;&quot;&gt;
-&lt;p style=&quot;-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-size:7.8pt;&quot;&gt;&lt;br /&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;Click a notification above to get more details...&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
</widget>
</item>
diff --git a/src/processrunner.cpp b/src/processrunner.cpp
new file mode 100644
index 00000000..91443750
--- /dev/null
+++ b/src/processrunner.cpp
@@ -0,0 +1,959 @@
+#include "processrunner.h"
+#include "organizercore.h"
+#include "instancemanager.h"
+#include "iuserinterface.h"
+#include "envmodule.h"
+#include "env.h"
+#include <iplugingame.h>
+#include <log.h>
+
+using namespace MOBase;
+
+void adjustForVirtualized(
+ const IPluginGame* game, spawn::SpawnParameters& sp, const Settings& settings)
+{
+ const QString modsPath = settings.paths().mods();
+
+ // Check if this a request with either an executable or a working directory
+ // under our mods folder then will start the process in a virtualized
+ // "environment" with the appropriate paths fixed:
+ // (i.e. mods\FNIS\path\exe => game\data\path\exe)
+ QString cwdPath = sp.currentDirectory.absolutePath();
+ bool virtualizedCwd = cwdPath.startsWith(modsPath, Qt::CaseInsensitive);
+ QString binPath = sp.binary.absoluteFilePath();
+ bool virtualizedBin = binPath.startsWith(modsPath, Qt::CaseInsensitive);
+ if (virtualizedCwd || virtualizedBin) {
+ if (virtualizedCwd) {
+ int cwdOffset = cwdPath.indexOf('/', modsPath.length() + 1);
+ QString adjustedCwd = cwdPath.mid(cwdOffset, -1);
+ cwdPath = game->dataDirectory().absolutePath();
+ if (cwdOffset >= 0)
+ cwdPath += adjustedCwd;
+
+ }
+
+ if (virtualizedBin) {
+ int binOffset = binPath.indexOf('/', modsPath.length() + 1);
+ QString adjustedBin = binPath.mid(binOffset, -1);
+ binPath = game->dataDirectory().absolutePath();
+ if (binOffset >= 0)
+ binPath += adjustedBin;
+ }
+
+ QString cmdline
+ = QString("launch \"%1\" \"%2\" %3")
+ .arg(QDir::toNativeSeparators(cwdPath),
+ QDir::toNativeSeparators(binPath), sp.arguments);
+
+ sp.binary = QFileInfo(QCoreApplication::applicationFilePath());
+ sp.arguments = cmdline;
+ sp.currentDirectory.setPath(QCoreApplication::applicationDirPath());
+ }
+}
+
+std::optional<ProcessRunner::Results> singleWait(HANDLE handle, DWORD pid)
+{
+ if (handle == INVALID_HANDLE_VALUE) {
+ return ProcessRunner::Error;
+ }
+
+ const auto res = WaitForSingleObject(handle, 50);
+
+ switch (res)
+ {
+ case WAIT_OBJECT_0:
+ {
+ log::debug("process {} completed", pid);
+ return ProcessRunner::Completed;
+ }
+
+ case WAIT_TIMEOUT:
+ {
+ // still running
+ return {};
+ }
+
+ case WAIT_FAILED: // fall-through
+ default:
+ {
+ // error
+ const auto e = ::GetLastError();
+ log::error("failed waiting for {}, {}", pid, formatSystemMessage(e));
+ return ProcessRunner::Error;
+ }
+ }
+}
+
+enum class Interest
+{
+ None = 0,
+ Weak,
+ Strong
+};
+
+QString toString(Interest i)
+{
+ switch (i)
+ {
+ case Interest::Weak:
+ return "weak";
+
+ case Interest::Strong:
+ return "strong";
+
+ case Interest::None: // fall-through
+ default:
+ return "no";
+ }
+}
+
+
+struct InterestingProcess
+{
+ env::Process p;
+ Interest interest = Interest::None;
+ env::HandlePtr handle;
+};
+
+
+InterestingProcess findRandomProcess(const env::Process& root)
+{
+ for (auto&& c : root.children()) {
+ env::HandlePtr h = c.openHandleForWait();
+ if (h) {
+ return {c, Interest::Weak, std::move(h)};
+ }
+
+ auto r = findRandomProcess(c);
+ if (r.handle) {
+ return r;
+ }
+ }
+
+ return {};
+}
+
+// returns a process that's in the hidden list, or the top-level process if
+// they're all hidden; returns an invalid process if the list is empty
+//
+InterestingProcess findInterestingProcessInTrees(const env::Process& root)
+{
+ // Certain process names we wish to "hide" for aesthetic reason:
+ static const std::vector<QString> hiddenList = {
+ QFileInfo(QCoreApplication::applicationFilePath()).fileName(),
+ "conhost.exe"
+ };
+
+ if (root.children().empty()) {
+ return {};
+ }
+
+ auto isHidden = [&](auto&& p) {
+ for (auto h : hiddenList) {
+ if (p.name().contains(h, Qt::CaseInsensitive)) {
+ return true;
+ }
+ }
+
+ return false;
+ };
+
+
+ for (auto&& p : root.children()) {
+ if (!isHidden(p)) {
+ env::HandlePtr h = p.openHandleForWait();
+ if (h) {
+ return {p, Interest::Strong, std::move(h)};
+ }
+ }
+
+ auto r = findInterestingProcessInTrees(p);
+ if (r.interest == Interest::Strong) {
+ return r;
+ }
+ }
+
+ // everything is hidden, just pick the first one that can be used
+ return findRandomProcess(root);
+}
+
+void dump(const env::Process& p, int indent)
+{
+ log::debug(
+ "{}{}, pid={}, ppid={}",
+ std::string(indent * 4, ' '), p.name(), p.pid(), p.ppid());
+
+ for (auto&& c : p.children()) {
+ dump(c, indent + 1);
+ }
+}
+
+void dump(const env::Process& root)
+{
+ log::debug("process tree:");
+
+ for (auto&& p : root.children()) {
+ dump(p, 1);
+ }
+}
+
+// gets the most interesting process in the list
+//
+InterestingProcess getInterestingProcess(HANDLE job)
+{
+ env::Process root = env::getProcessTree(job);
+ if (root.children().empty()) {
+ log::debug("nothing to wait for");
+ return {};
+ }
+
+ dump(root);
+
+ auto interest = findInterestingProcessInTrees(root);
+ if (!interest.handle) {
+ // this can happen if none of the processes can be opened
+ log::debug("no interesting process to wait for");
+ return {};
+ }
+
+ return interest;
+}
+
+const std::chrono::milliseconds Infinite(-1);
+
+// waits for completion, times out after `wait` if not Infinite
+//
+std::optional<ProcessRunner::Results> timedWait(
+ HANDLE handle, DWORD pid, UILocker::Session& ls,
+ std::chrono::milliseconds wait, std::atomic<bool>& interrupt)
+{
+ using namespace std::chrono;
+
+ high_resolution_clock::time_point start;
+ if (wait != Infinite) {
+ start = high_resolution_clock::now();
+ }
+
+ while (!interrupt) {
+ // wait for a very short while, allows for processing events below
+ const auto r = singleWait(handle, pid);
+
+ if (r) {
+ // the process has either completed or an error was returned
+ return *r;
+ }
+
+ // the process is still running
+
+ // check the lock widget
+ switch (ls.result())
+ {
+ case UILocker::StillLocked:
+ {
+ break;
+ }
+
+ case UILocker::ForceUnlocked:
+ {
+ log::debug("waiting for {} force unlocked by user", pid);
+ return ProcessRunner::ForceUnlocked;
+ }
+
+ case UILocker::Cancelled:
+ {
+ log::debug("waiting for {} cancelled by user", pid);
+ return ProcessRunner::Cancelled;
+ }
+
+ case UILocker::NoResult: // fall-through
+ default:
+ {
+ // shouldn't happen
+ log::debug(
+ "unexpected result {} while waiting for {}",
+ static_cast<int>(ls.result()), pid);
+
+ return ProcessRunner::Error;
+ }
+ }
+
+ if (wait != Infinite) {
+ // check if enough time has elapsed
+ const auto now = high_resolution_clock::now();
+ if (duration_cast<milliseconds>(now - start) >= wait) {
+ // if so, return an empty result
+ return {};
+ }
+ }
+ }
+
+ log::debug("waiting for {} interrupted", pid);
+ return ProcessRunner::ForceUnlocked;
+}
+
+ProcessRunner::Results waitForProcessesThreadImpl(
+ HANDLE job, UILocker::Session& ls, std::atomic<bool>& interrupt)
+{
+ using namespace std::chrono;
+
+ DWORD currentPID = 0;
+
+ // if the interesting process that was found is weak (such as ModOrganizer.exe
+ // when starting a program from within the Data directory), start with a short
+ // wait and check for more interesting children
+ const milliseconds defaultWait(50);
+ auto wait = defaultWait;
+
+ while (!interrupt) {
+ auto ip = getInterestingProcess(job);
+ if (!ip.handle) {
+ // nothing to wait on
+ return ProcessRunner::Completed;
+ }
+
+ // update the lock widget
+ ls.setInfo(ip.p.pid(), ip.p.name());
+
+ if (ip.p.pid() != currentPID) {
+ // log any change in the process being waited for
+ currentPID = ip.p.pid();
+
+ log::debug(
+ "waiting for completion on {} ({}), {} interest",
+ ip.p.name(), ip.p.pid(), toString(ip.interest));
+ }
+
+ if (ip.interest == Interest::Strong) {
+ // don't bother with short wait, this is a good process to wait for
+ wait = Infinite;
+ }
+
+ const auto r = timedWait(ip.handle.get(), ip.p.pid(), ls, wait, interrupt);
+ if (r) {
+ if (*r == ProcessRunner::Results::Completed) {
+ // process completed, check another one, reset the wait time to find
+ // interesting processes
+ wait = defaultWait;
+ } else if (*r != ProcessRunner::Results::Running) {
+ // something's wrong, or the user unlocked the ui
+ return *r;
+ }
+ }
+
+ // exponentially increase the wait time between checks for interesting
+ // processes
+ wait = std::min(wait * 2, milliseconds(2000));
+ }
+
+ log::debug("waiting for processes interrupted");
+ return ProcessRunner::ForceUnlocked;
+}
+
+void waitForProcessesThread(
+ ProcessRunner::Results& result, HANDLE job, UILocker::Session& ls,
+ std::atomic<bool>& interrupt)
+{
+ result = waitForProcessesThreadImpl(job, ls, interrupt);
+ ls.unlock();
+}
+
+ProcessRunner::Results waitForProcesses(
+ const std::vector<HANDLE>& initialProcesses, UILocker::Session& ls)
+{
+ if (initialProcesses.empty()) {
+ // nothing to wait for
+ return ProcessRunner::Completed;
+ }
+
+ // using a job so any child process started by any of those processes can also
+ // be captured and monitored
+ env::HandlePtr job(CreateJobObjectW(nullptr, nullptr));
+ if (!job) {
+ const auto e = GetLastError();
+
+ log::error(
+ "failed to create job to wait for processes, {}",
+ formatSystemMessage(e));
+
+ return ProcessRunner::Error;
+ }
+
+ bool oneWorked = false;
+
+ for (auto&& h : initialProcesses) {
+ if (::AssignProcessToJobObject(job.get(), h)) {
+ oneWorked = true;
+ } else {
+ const auto e = GetLastError();
+
+ // this happens when closing MO while multiple processes are running,
+ // so the logging is disabled until it gets fixed
+
+ //log::error(
+ // "can't assign process to job to wait for processes, {}",
+ // formatSystemMessage(e));
+
+ // keep going
+ }
+ }
+
+ HANDLE monitor = INVALID_HANDLE_VALUE;
+
+ if (oneWorked) {
+ monitor = job.get();
+ } else {
+ // none of the handles could be added to the job, just monitor the first one
+ monitor = initialProcesses[0];
+ }
+
+ auto results = ProcessRunner::Running;
+ std::atomic<bool> interrupt(false);
+
+ auto* t = QThread::create(
+ waitForProcessesThread,
+ std::ref(results), monitor, std::ref(ls), std::ref(interrupt));
+
+ QEventLoop events;
+ QObject::connect(t, &QThread::finished, [&]{
+ events.quit();
+ });
+
+ t->start();
+ events.exec();
+
+ if (t->isRunning()) {
+ interrupt = true;
+ t->wait();
+ }
+
+ delete t;
+
+ return results;
+}
+
+ProcessRunner::Results waitForProcess(
+ HANDLE initialProcess, LPDWORD exitCode, UILocker::Session& ls)
+{
+ std::vector<HANDLE> processes = {initialProcess};
+
+ const auto r = waitForProcesses(processes, ls);
+
+ // as long as it's not running anymore, try to get the exit code
+ if (exitCode && r != ProcessRunner::Running) {
+ if (!::GetExitCodeProcess(initialProcess, exitCode)) {
+ const auto e = ::GetLastError();
+ log::warn(
+ "failed to get exit code of process, {}",
+ formatSystemMessage(e));
+ }
+ }
+
+ return r;
+}
+
+
+ProcessRunner::ProcessRunner(OrganizerCore& core, IUserInterface* ui) :
+ m_core(core), m_ui(ui), m_lockReason(UILocker::NoReason),
+ m_waitFlags(NoFlags), m_handle(INVALID_HANDLE_VALUE), m_exitCode(-1)
+{
+ // all processes started in ProcessRunner are hooked by default
+ setHooked(true);
+}
+
+ProcessRunner& ProcessRunner::setBinary(const QFileInfo &binary)
+{
+ m_sp.binary = binary;
+ return *this;
+}
+
+ProcessRunner& ProcessRunner::setArguments(const QString& arguments)
+{
+ m_sp.arguments = arguments;
+ return *this;
+}
+
+ProcessRunner& ProcessRunner::setCurrentDirectory(const QDir& directory)
+{
+ m_sp.currentDirectory = directory;
+ return *this;
+}
+
+ProcessRunner& ProcessRunner::setSteamID(const QString& steamID)
+{
+ m_sp.steamAppID = steamID;
+ return *this;
+}
+
+ProcessRunner& ProcessRunner::setCustomOverwrite(const QString& customOverwrite)
+{
+ m_customOverwrite = customOverwrite;
+ return *this;
+}
+
+ProcessRunner& ProcessRunner::setForcedLibraries(const ForcedLibraries& forcedLibraries)
+{
+ m_forcedLibraries = forcedLibraries;
+ return *this;
+}
+
+ProcessRunner& ProcessRunner::setProfileName(const QString& profileName)
+{
+ m_profileName = profileName;
+ return *this;
+}
+
+ProcessRunner& ProcessRunner::setWaitForCompletion(
+ WaitFlags flags, UILocker::Reasons reason)
+{
+ m_waitFlags = flags;
+ m_lockReason = reason;
+ return *this;
+}
+
+ProcessRunner& ProcessRunner::setHooked(bool b)
+{
+ m_sp.hooked = b;
+ return *this;
+}
+
+ProcessRunner& ProcessRunner::setFromFile(
+ QWidget* parent, const QFileInfo& targetInfo)
+{
+ if (!parent && m_ui) {
+ parent = m_ui->qtWidget();
+ }
+
+ // if the file is a .exe, start it directly; if it's anything else, ask the
+ // shell to start it
+
+ const auto fec = spawn::getFileExecutionContext(parent, targetInfo);
+
+ switch (fec.type)
+ {
+ case spawn::FileExecutionTypes::Executable:
+ {
+ setBinary(fec.binary);
+ setArguments(fec.arguments);
+ setCurrentDirectory(targetInfo.absoluteDir());
+ break;
+ }
+
+ case spawn::FileExecutionTypes::Other: // fall-through
+ default:
+ {
+ m_shellOpen = targetInfo;
+ setHooked(false);
+ break;
+ }
+ }
+
+ return *this;
+}
+
+ProcessRunner& ProcessRunner::setFromExecutable(const Executable& exe)
+{
+ const auto* profile = m_core.currentProfile();
+ if (!profile) {
+ throw MyException(QObject::tr("No profile set"));
+ }
+
+ const QString customOverwrite = profile->setting(
+ "custom_overwrites", exe.title()).toString();
+
+ ForcedLibraries forcedLibraries;
+ if (profile->forcedLibrariesEnabled(exe.title())) {
+ forcedLibraries = profile->determineForcedLibraries(exe.title());
+ }
+
+ QString currentDirectory = exe.workingDirectory();
+ if (currentDirectory.isEmpty()) {
+ currentDirectory = exe.binaryInfo().absolutePath();
+ }
+
+ setBinary(exe.binaryInfo());
+ setArguments(exe.arguments());
+ setCurrentDirectory(currentDirectory);
+ setSteamID(exe.steamAppID());
+ setCustomOverwrite(customOverwrite);
+ setForcedLibraries(forcedLibraries);
+
+ return *this;
+}
+
+ProcessRunner& ProcessRunner::setFromShortcut(const MOShortcut& shortcut)
+{
+ const auto currentInstance = InstanceManager::instance().currentInstance();
+
+ if (shortcut.hasInstance() && shortcut.instance() != currentInstance) {
+ throw std::runtime_error(
+ QString("Refusing to run executable from different instance %1:%2")
+ .arg(shortcut.instance(),shortcut.executable())
+ .toLocal8Bit().constData());
+ }
+
+ const Executable& exe = m_core.executablesList()->get(shortcut.executable());
+ setFromExecutable(exe);
+
+ return *this;
+}
+
+ProcessRunner& ProcessRunner::setFromFileOrExecutable(
+ const QString &executable,
+ const QStringList &args,
+ const QString &cwd,
+ const QString &profileOverride,
+ const QString &forcedCustomOverwrite,
+ bool ignoreCustomOverwrite)
+{
+ const auto* profile = m_core.currentProfile();
+ if (!profile) {
+ throw MyException(QObject::tr("No profile set"));
+ }
+
+ setBinary(executable);
+ setArguments(args.join(" "));
+ setCurrentDirectory(cwd);
+ setProfileName(profileOverride);
+
+ if (executable.contains('\\') || executable.contains('/')) {
+ // file path
+
+ if (m_sp.binary.isRelative()) {
+ // relative path, should be relative to game directory
+ setBinary(m_core.managedGame()->gameDirectory().absoluteFilePath(executable));
+ }
+
+ if (cwd == "") {
+ setCurrentDirectory(m_sp.binary.absolutePath());
+ }
+
+ try {
+ const Executable& exe = m_core.executablesList()->getByBinary(m_sp.binary);
+
+ setSteamID(exe.steamAppID());
+ setCustomOverwrite(profile->setting("custom_overwrites", exe.title()).toString());
+
+ if (profile->forcedLibrariesEnabled(exe.title())) {
+ setForcedLibraries(profile->determineForcedLibraries(exe.title()));
+ }
+ } catch (const std::runtime_error &) {
+ // nop
+ }
+ } else {
+ // only a file name, search executables list
+ try {
+ const Executable &exe = m_core.executablesList()->get(executable);
+
+ setSteamID(exe.steamAppID());
+ setCustomOverwrite(profile->setting("custom_overwrites", exe.title()).toString());
+
+ if (profile->forcedLibrariesEnabled(exe.title())) {
+ setForcedLibraries(profile->determineForcedLibraries(exe.title()));
+ }
+
+ if (args.isEmpty()) {
+ setArguments(exe.arguments());
+ }
+
+ setBinary(exe.binaryInfo());
+
+ if (cwd == "") {
+ setCurrentDirectory(exe.workingDirectory());
+ }
+ } catch (const std::runtime_error &) {
+ log::warn("\"{}\" not set up as executable", executable);
+ }
+ }
+
+ if (ignoreCustomOverwrite) {
+ setCustomOverwrite("");
+ } else if (!forcedCustomOverwrite.isEmpty()) {
+ setCustomOverwrite(forcedCustomOverwrite);
+ }
+
+ return *this;
+}
+
+bool ProcessRunner::shouldRunShell() const
+{
+ return !m_shellOpen.filePath().isEmpty();
+}
+
+ProcessRunner::Results ProcessRunner::run()
+{
+ // check if setHooked() was called after setFromFile(); this needs to
+ // modify the settings to run the associated executable instead of using
+ // shell::Open()
+
+ if (shouldRunShell() && m_sp.hooked) {
+ // this is a non-executable file, but it should be hooked; the associated
+ // executable needs to be retrieved and run instead
+ auto assoc = env::getAssociation(m_shellOpen);
+ if (!assoc.executable.filePath().isEmpty()) {
+ setBinary(assoc.executable);
+ setArguments(assoc.formattedCommandLine);
+ setCurrentDirectory(assoc.executable.absoluteDir());
+ m_shellOpen = {};
+ } else {
+ // if it fails, just use the regular shell open
+ log::error("failed to get the associated executable, running unhooked");
+ m_sp.hooked = false;
+ }
+ } else if (!shouldRunShell() && !m_sp.hooked) {
+ // this is an executable that should not be hooked; just run it through
+ // the shell
+ m_shellOpen = m_sp.binary;
+ }
+
+
+ std::optional<Results> r;
+
+ if (shouldRunShell()) {
+ r = runShell();
+ } else {
+ r = runBinary();
+ }
+
+ if (r) {
+ // early result: something went wrong and the process cannot be waited for
+ return *r;
+ }
+
+ return postRun();
+}
+
+std::optional<ProcessRunner::Results> ProcessRunner::runShell()
+{
+ const auto file = m_shellOpen.absoluteFilePath();
+
+ log::debug("executing from shell: '{}'", file);
+
+ auto r = shell::Open(file);
+ if (!r.success()) {
+ return Error;
+ }
+
+ m_handle.reset(r.stealProcessHandle());
+
+ // not all files will return a valid handle even if opening them was
+ // successful, such as inproc handlers (like the photo viewer); in this
+ // case it's impossible to determine the status, so just say it's still
+ // running
+ if (m_handle.get() == INVALID_HANDLE_VALUE) {
+ log::debug("shell didn't report an error, but no handle is available");
+ return Running;
+ }
+
+ return {};
+}
+
+std::optional<ProcessRunner::Results> ProcessRunner::runBinary()
+{
+ if (m_profileName.isEmpty()) {
+ // get the current profile name if it wasn't overridden
+ const auto* profile = m_core.currentProfile();
+ if (!profile) {
+ throw MyException(QObject::tr("No profile set"));
+ }
+
+ m_profileName = profile->name();
+ }
+
+ // saves profile, sets up usvfs, notifies plugins, etc.; can return false if
+ // a plugin doesn't want the program to run (such as when checkFNIS fails to
+ // run FNIS and the user clicks cancel)
+ if (!m_core.beforeRun(m_sp.binary, m_profileName, m_customOverwrite, m_forcedLibraries)) {
+ return Error;
+ }
+
+ // parent widget used for any dialog popped up while checking for things
+ QWidget* parent = (m_ui ? m_ui->qtWidget() : nullptr);
+
+ const auto* game = m_core.managedGame();
+ auto& settings = m_core.settings();
+
+ // start steam if needed
+ if (!checkSteam(parent, m_sp, game->gameDirectory(), m_sp.steamAppID, settings)) {
+ return Error;
+ }
+
+ // warn if the executable is on the blacklist
+ if (!checkBlacklist(parent, m_sp, settings)) {
+ return Error;
+ }
+
+ // if the executable is inside the mods folder another instance of
+ // ModOrganizer.exe is spawned instead to launch it
+ adjustForVirtualized(game, m_sp, settings);
+
+ // run the binary
+ m_handle.reset(startBinary(parent, m_sp));
+ if (m_handle.get() == INVALID_HANDLE_VALUE) {
+ return Error;
+ }
+
+ return {};
+}
+
+bool ProcessRunner::shouldRefresh(Results r) const
+{
+ // afterRun() is only called with the Refresh flag; it refreshes the
+ // directory structure and notifies plugins
+ //
+ // refreshing is not always required and can actually cause problems:
+ //
+ // 1) running shortcuts doesn't need refreshing because MO closes right
+ // after
+ //
+ // 2) the mod info dialog is not set up to deal with refreshes, so that
+ // it will crash because the old DirectoryEntry's are still being used
+ // in the list
+ if (!m_waitFlags.testFlag(Refresh)) {
+ log::debug("not refreshing because the flag isn't set");
+ return false;
+ }
+
+ switch (r)
+ {
+ case Completed:
+ {
+ log::debug("refreshing because the process completed");
+ return true;
+ }
+
+ case ForceUnlocked:
+ {
+ log::debug("refreshing because the ui was force unlocked");
+ return true;
+ }
+
+ case Error: // fall-through
+ case Cancelled:
+ case Running:
+ default:
+ {
+ return false;
+ }
+ }
+}
+
+ProcessRunner::Results ProcessRunner::postRun()
+{
+ const bool mustWait = (m_waitFlags & ForceWait);
+
+ if (!m_sp.hooked && !mustWait) {
+ // the process wasn't hooked and there's no force wait, don't lock
+ return Running;
+ }
+
+ if (mustWait && m_lockReason == UILocker::NoReason) {
+ // never lock the ui without an escape hatch for the user
+ log::debug(
+ "the ForceWait flag is set but the lock reason wasn't, "
+ "defaulting to LockUI");
+
+ m_lockReason = UILocker::LockUI;
+ }
+
+ if (mustWait) {
+ if (!m_core.settings().interface().lockGUI()) {
+ // at least tell the user what's going on
+ log::debug(
+ "locking is disabled, but the output of the application is required; "
+ "overriding this setting and locking the ui");
+ }
+ } else {
+ // no force wait
+
+ if (m_lockReason == UILocker::NoReason) {
+ // no locking requested
+ return Running;
+ }
+
+ if (!m_core.settings().interface().lockGUI()) {
+ // disabling locking is like clicking on unlock immediately
+ log::debug("not waiting for process because locking is disabled");
+ return ForceUnlocked;
+ }
+ }
+
+ auto r = Error;
+
+ withLock([&](auto& ls) {
+ r = waitForProcess(m_handle.get(), &m_exitCode, ls);
+ });
+
+ if (shouldRefresh(r)) {
+ m_core.afterRun(m_sp.binary, m_exitCode);
+ }
+
+ return r;
+}
+
+ProcessRunner::Results ProcessRunner::attachToProcess(HANDLE h)
+{
+ m_handle.reset(h);
+ return postRun();
+}
+
+DWORD ProcessRunner::exitCode() const
+{
+ return m_exitCode;
+}
+
+HANDLE ProcessRunner::getProcessHandle() const
+{
+ return m_handle.get();
+}
+
+env::HandlePtr ProcessRunner::stealProcessHandle()
+{
+ auto h = m_handle.release();
+ m_handle.reset(INVALID_HANDLE_VALUE);
+ return env::HandlePtr(h);
+}
+
+ProcessRunner::Results ProcessRunner::waitForAllUSVFSProcessesWithLock(
+ UILocker::Reasons reason)
+{
+ m_lockReason = reason;
+
+ if (!m_core.settings().interface().lockGUI()) {
+ // disabling locking is like clicking on unlock immediately
+ return ForceUnlocked;
+ }
+
+ auto r = Error;
+
+ for (;;) {
+ withLock([&](auto& ls) {
+ const auto processes = getRunningUSVFSProcesses();
+ if (processes.empty()) {
+ r = Completed;
+ return;
+ }
+
+ r = waitForProcesses(processes, ls);
+
+ if (r != Completed) {
+ // error, cancelled, or unlocked
+ return;
+ }
+
+ // this process is completed, check for others
+ r = Running;
+ });
+
+ if (r != Running) {
+ break;
+ }
+ }
+
+ return r;
+}
+
+void ProcessRunner::withLock(std::function<void (UILocker::Session&)> f)
+{
+ auto ls = UILocker::instance().lock(m_lockReason);
+ f(*ls);
+}
diff --git a/src/processrunner.h b/src/processrunner.h
new file mode 100644
index 00000000..d8ff0227
--- /dev/null
+++ b/src/processrunner.h
@@ -0,0 +1,180 @@
+#ifndef PROCESSRUNNER_H
+#define PROCESSRUNNER_H
+
+#include "spawn.h"
+#include "uilocker.h"
+#include "envmodule.h"
+#include <executableinfo.h>
+
+class OrganizerCore;
+class IUserInterface;
+class Executable;
+class MOShortcut;
+
+// handles spawning a process and waiting for it, including setting up the lock
+// widget if required
+//
+class ProcessRunner
+{
+public:
+ enum Results
+ {
+ // the process is still running
+ Running = 1,
+
+ // the process has run to completion
+ Completed,
+
+ // the process couldn't be started or waited for
+ Error,
+
+ // the user has clicked the cancel button in the lock widget
+ Cancelled,
+
+ // the user has clicked the unlock button in the lock widget
+ ForceUnlocked
+ };
+
+ enum WaitFlag
+ {
+ NoFlags = 0x00,
+
+ // the ui will be refreshed once the process has completed
+ Refresh = 0x01,
+
+ // the process will be waited for even if locking is disabled or the
+ // process is not hooked
+ ForceWait = 0x02,
+ };
+
+ using WaitFlags = QFlags<WaitFlag>;
+ using ForcedLibraries = QList<MOBase::ExecutableForcedLoadSetting>;
+
+ ProcessRunner(OrganizerCore& core, IUserInterface* ui);
+
+ // move only
+ ProcessRunner(ProcessRunner&&) = default;
+ ProcessRunner& operator=(const ProcessRunner&) = delete;
+ ProcessRunner(const ProcessRunner&) = delete;
+ ProcessRunner& operator=(ProcessRunner&&) = delete;
+
+ ProcessRunner& setBinary(const QFileInfo &binary);
+ ProcessRunner& setArguments(const QString& arguments);
+ ProcessRunner& setCurrentDirectory(const QDir& directory);
+ ProcessRunner& setSteamID(const QString& steamID);
+ ProcessRunner& setCustomOverwrite(const QString& customOverwrite);
+ ProcessRunner& setForcedLibraries(const ForcedLibraries& forcedLibraries);
+ ProcessRunner& setProfileName(const QString& profileName);
+ ProcessRunner& setWaitForCompletion(
+ WaitFlags flags=NoFlags, UILocker::Reasons reason=UILocker::LockUI);
+ ProcessRunner& setHooked(bool b);
+
+ // - if the target is an executable file, runs it hooked
+ // - if the target is a file:
+ // - if forceHook is false, calls ShellExecute() on it
+ // - if forceHook is true, gets the executable associated with the file
+ // and runs that hooked by passing the file as an argument
+ //
+ ProcessRunner& setFromFile(QWidget* parent, const QFileInfo& targetInfo);
+
+ ProcessRunner& setFromExecutable(const Executable& exe);
+ ProcessRunner& setFromShortcut(const MOShortcut& shortcut);
+
+ // this is a messy one that's used for running an arbitrary file from the
+ // command line, or by plugins (see OrganizerProxy::startApplication())
+ //
+ // 1) if `executable` contains a path separator, it's treated as a binary on
+ // disk and will be launched with given settings; it's also looked up in
+ // the list of configured executables, which sets the steam ID and forced
+ // libraries
+ //
+ // 2) if `executable` has no path separators, it's treated purely as an
+ // executable, but its arguments, current directory and custom overwrite
+ // can also be overridden
+ //
+ // if the executable is not found in the list, the binary is run solely
+ // based on the parameters given
+ //
+ ProcessRunner& setFromFileOrExecutable(
+ const QString &executable,
+ const QStringList &args,
+ const QString &cwd={},
+ const QString &profile={},
+ const QString &forcedCustomOverwrite={},
+ bool ignoreCustomOverwrite=false);
+
+ // spawns the process and waits for it if required
+ //
+ Results run();
+
+ // takes ownership of the given handle and waits for it if required
+ //
+ Results attachToProcess(HANDLE h);
+
+ // exit code of the process, will return -1 if the process wasn't waited for
+ //
+ DWORD exitCode() const;
+
+ // this may be INVALID_HANDLE_VALUE if:
+ //
+ // 1) no process was started, or
+ // 2) the process was started successfully, but the system didn't return a
+ // handle for it; this can happen for inproc handlers, for example, such
+ // the photo viewer
+ //
+ // note that the handle is still owned by this ProcessRunner and will be
+ // closed when destroyed; see stealProcessHandle()
+ //
+ HANDLE getProcessHandle() const;
+
+ // releases ownership of the process handle; if this is called after the
+ // process is completed, exitCode() will still return the correct value
+ //
+ env::HandlePtr stealProcessHandle();
+
+ // waits for all usvfs processes spawned by this instance of MO; returns
+ // immediately with ForceUnlocked if locking is disabled
+ //
+ // strictly speaking, this shouldn't be here, as it has nothing to do with
+ // running a process, but it uses the same internal stuff as when running a
+ // process
+ //
+ Results waitForAllUSVFSProcessesWithLock(UILocker::Reasons reason);
+
+private:
+ OrganizerCore& m_core;
+ IUserInterface* m_ui;
+ spawn::SpawnParameters m_sp;
+ QString m_customOverwrite;
+ ForcedLibraries m_forcedLibraries;
+ QString m_profileName;
+ UILocker::Reasons m_lockReason;
+ WaitFlags m_waitFlags;
+ QFileInfo m_shellOpen;
+ env::HandlePtr m_handle;
+ DWORD m_exitCode;
+
+
+ bool shouldRunShell() const;
+ bool shouldRefresh(Results r) const;
+
+ // runs the command in m_shellOpen; returns empty if it can be waited for
+ //
+ std::optional<Results> runShell();
+
+ // runs the binary; returns empty if it can be waited for
+ //
+ std::optional<Results> runBinary();
+
+ // waits for process completion if required
+ //
+ Results postRun();
+
+ // creates the lock widget and calls f()
+ //
+ void withLock(std::function<void (UILocker::Session&)> f);
+};
+
+Q_DECLARE_OPERATORS_FOR_FLAGS(ProcessRunner::WaitFlags);
+
+#endif // PROCESSRUNNER_H
diff --git a/src/profile.cpp b/src/profile.cpp
index ef387027..f041a241 100644
--- a/src/profile.cpp
+++ b/src/profile.cpp
@@ -40,7 +40,6 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#include <QMessageBox>
#include <QScopedArrayPointer>
#include <QStringList> // for QStringList
-#include <QtDebug> // for qDebug, qWarning, etc
#include <QtGlobal> // for qUtf8Printable
#include <QBuffer>
#include <QDirIterator>
@@ -74,7 +73,7 @@ Profile::Profile(const QString &name, IPluginGame const *gamePlugin, bool useDef
: m_ModListWriter(std::bind(&Profile::doWriteModlist, this))
, m_GamePlugin(gamePlugin)
{
- QString profilesDir = Settings::instance().getProfileDirectory();
+ QString profilesDir = Settings::instance().paths().profiles();
QDir profileBase(profilesDir);
QString fixedName = name;
if (!fixDirectoryName(fixedName)) {
@@ -125,7 +124,7 @@ Profile::Profile(const QDir &directory, IPluginGame const *gamePlugin)
findProfileSettings();
if (!QFile::exists(m_Directory.filePath("modlist.txt"))) {
- qWarning("missing modlist.txt in %s", qUtf8Printable(directory.path()));
+ log::warn("missing modlist.txt in {}", directory.path());
touchFile(m_Directory.filePath("modlist.txt"));
}
@@ -158,42 +157,42 @@ Profile::~Profile()
void Profile::findProfileSettings()
{
- if (setting("LocalSaves") == QVariant()) {
+ if (setting("", "LocalSaves") == QVariant()) {
if (m_Directory.exists("saves")) {
- storeSetting("LocalSaves", true);
+ storeSetting("", "LocalSaves", true);
} else {
if (m_Directory.exists("_saves")) {
m_Directory.rename("_saves", "saves");
}
- storeSetting("LocalSaves", false);
+ storeSetting("", "LocalSaves", false);
}
}
- if (setting("LocalSettings") == QVariant()) {
+ if (setting("", "LocalSettings") == QVariant()) {
QString backupFile = getIniFileName() + "_";
if (m_Directory.exists(backupFile)) {
- storeSetting("LocalSettings", false);
+ storeSetting("", "LocalSettings", false);
m_Directory.rename(backupFile, getIniFileName());
} else {
- storeSetting("LocalSettings", true);
+ storeSetting("", "LocalSettings", true);
}
}
- if (setting("AutomaticArchiveInvalidation") == QVariant()) {
+ if (setting("", "AutomaticArchiveInvalidation") == QVariant()) {
BSAInvalidation *invalidation = m_GamePlugin->feature<BSAInvalidation>();
DataArchives *dataArchives = m_GamePlugin->feature<DataArchives>();
bool found = false;
if ((invalidation != nullptr) && (dataArchives != nullptr)) {
for (const QString &archive : dataArchives->archives(this)) {
if (invalidation->isInvalidationBSA(archive)) {
- storeSetting("AutomaticArchiveInvalidation", true);
+ storeSetting("", "AutomaticArchiveInvalidation", true);
found = true;
break;
}
}
}
if (!found) {
- storeSetting("AutomaticArchiveInvalidation", false);
+ storeSetting("", "AutomaticArchiveInvalidation", false);
}
}
}
@@ -232,7 +231,6 @@ void Profile::doWriteModlist()
}
for (std::map<int, unsigned int>::const_reverse_iterator iter = m_ModIndexByPriority.crbegin(); iter != m_ModIndexByPriority.crend(); iter++ ) {
- //qDebug(QString("write mod %1 to priority %2").arg(iter->first).arg(iter->second).toLocal8Bit());
// the priority order was inverted on load so it has to be inverted again
unsigned int index = iter->second;
if (index != UINT_MAX) {
@@ -253,7 +251,7 @@ void Profile::doWriteModlist()
}
if (file.commitIfDifferent(m_LastModlistHash)) {
- qDebug("%s saved", qUtf8Printable(QDir::toNativeSeparators(fileName)));
+ log::debug("{} saved", QDir::toNativeSeparators(fileName));
}
} catch (const std::exception &e) {
reportError(tr("failed to write mod list: %1").arg(e.what()));
@@ -267,7 +265,10 @@ void Profile::createTweakedIniFile()
QString tweakedIni = m_Directory.absoluteFilePath("initweaks.ini");
if (QFile::exists(tweakedIni) && !shellDeleteQuiet(tweakedIni)) {
- reportError(tr("failed to update tweaked ini file, wrong settings may be used: %1").arg(windowsErrorString(::GetLastError())));
+ const auto e = GetLastError();
+ reportError(
+ tr("failed to update tweaked ini file, wrong settings may be used: %1")
+ .arg(QString::fromStdWString(formatSystemMessage(e))));
return;
}
@@ -287,15 +288,18 @@ void Profile::createTweakedIniFile()
}
if (error) {
- reportError(tr("failed to create tweaked ini: %1").arg(getCurrentErrorString().c_str()));
+ const auto e = ::GetLastError();
+ reportError(tr("failed to create tweaked ini: %1")
+ .arg(QString::fromStdWString(formatSystemMessage(e))));
}
- qDebug("%s saved", qUtf8Printable(QDir::toNativeSeparators(tweakedIni)));
+
+ log::debug("{} saved", QDir::toNativeSeparators(tweakedIni));
}
// static
void Profile::renameModInAllProfiles(const QString& oldName, const QString& newName)
{
- QDir profilesDir(Settings::instance().getProfileDirectory());
+ QDir profilesDir(Settings::instance().paths().profiles());
profilesDir.setFilter(QDir::AllDirs | QDir::NoDotAndDotDot);
QDirIterator profileIter(profilesDir);
while (profileIter.hasNext()) {
@@ -304,7 +308,7 @@ void Profile::renameModInAllProfiles(const QString& oldName, const QString& newN
if (modList.exists())
renameModInList(modList, oldName, newName);
else
- qWarning("Profile has no modlist.txt : %s", qUtf8Printable(profileIter.filePath()));
+ log::warn("Profile has no modlist.txt: {}", profileIter.filePath());
}
}
@@ -325,7 +329,7 @@ void Profile::renameModInList(QFile &modList, const QString &oldName, const QStr
if (line.length() == 0) {
// ignore empty lines
- qWarning("mod list contained invalid data: empty line");
+ log::warn("mod list contained invalid data: empty line");
continue;
}
@@ -340,7 +344,7 @@ void Profile::renameModInList(QFile &modList, const QString &oldName, const QStr
if (modName.isEmpty()) {
// file broken?
- qWarning("mod list contained invalid data: missing mod name");
+ log::warn("mod list contained invalid data: missing mod name");
continue;
}
@@ -361,8 +365,9 @@ void Profile::renameModInList(QFile &modList, const QString &oldName, const QStr
}
if (renamed)
- qDebug("Renamed %d \"%s\" mod to \"%s\" in %s",
- renamed, qUtf8Printable(oldName), qUtf8Printable(newName), qUtf8Printable(modList.fileName()));
+ log::debug(
+ "Renamed {} \"{}\" mod to \"{}\" in {}",
+ renamed, oldName, newName, modList.fileName());
}
void Profile::refreshModStatus()
@@ -421,14 +426,16 @@ void Profile::refreshModStatus()
m_ModStatus[modIndex].m_Priority = index++;
}
} else {
- qWarning("no mod state for \"%s\" (profile \"%s\")",
- qUtf8Printable(modName), qUtf8Printable(m_Directory.path()));
+ log::warn(
+ "no mod state for \"{}\" (profile \"{}\")",
+ modName, m_Directory.path());
// need to rewrite the modlist to fix this
modStatusModified = true;
}
} else {
- qDebug("mod not found: \"%s\" (profile \"%s\")",
- qUtf8Printable(modName), qUtf8Printable(m_Directory.path()));
+ log::debug(
+ "mod not found: \"{}\" (profile \"{}\")",
+ modName, m_Directory.path());
// need to rewrite the modlist to fix this
modStatusModified = true;
}
@@ -492,8 +499,10 @@ void Profile::dumpModStatus() const
{
for (unsigned int i = 0; i < m_ModStatus.size(); ++i) {
ModInfo::Ptr info = ModInfo::getByIndex(i);
- qWarning("%d: %s - %d (%s)", i, info->name().toUtf8().constData(), m_ModStatus[i].m_Priority,
- m_ModStatus[i].m_Enabled ? "enabled" : "disabled");
+ log::warn(
+ "{}: {} - {} ({})",
+ i, info->name(), m_ModStatus[i].m_Priority,
+ m_ModStatus[i].m_Enabled ? "enabled" : "disabled");
}
}
@@ -566,7 +575,7 @@ void Profile::setModsEnabled(const QList<unsigned int> &modsToEnable, const QLis
QList<unsigned int> dirtyMods;
for (auto idx : modsToEnable) {
if (idx >= m_ModStatus.size()) {
- qCritical() << tr("invalid mod index: %1").arg(idx);
+ log::error("invalid mod index: {}", idx);
continue;
}
if (!m_ModStatus[idx].m_Enabled) {
@@ -576,7 +585,7 @@ void Profile::setModsEnabled(const QList<unsigned int> &modsToEnable, const QLis
}
for (auto idx : modsToDisable) {
if (idx >= m_ModStatus.size()) {
- qCritical() << tr("invalid mod index: %1").arg(idx);
+ log::error("invalid mod index: {}", idx);
continue;
}
if (ModInfo::getByIndex(idx)->alwaysEnabled()) {
@@ -646,7 +655,7 @@ void Profile::setModPriority(unsigned int index, int &newPriority)
Profile *Profile::createPtrFrom(const QString &name, const Profile &reference, MOBase::IPluginGame const *gamePlugin)
{
- QString profileDirectory = Settings::instance().getProfileDirectory() + "/" + name;
+ QString profileDirectory = Settings::instance().paths().profiles() + "/" + name;
reference.copyFilesTo(profileDirectory);
return new Profile(QDir(profileDirectory), gamePlugin);
}
@@ -733,7 +742,7 @@ bool Profile::invalidationActive(bool *supported) const
*supported = ((invalidation != nullptr) && (dataArchives != nullptr));
}
- return setting("AutomaticArchiveInvalidation", false).toBool();
+ return setting("", "AutomaticArchiveInvalidation", false).toBool();
}
@@ -745,7 +754,7 @@ void Profile::deactivateInvalidation()
invalidation->deactivate(this);
}
- storeSetting("AutomaticArchiveInvalidation", false);
+ storeSetting("", "AutomaticArchiveInvalidation", false);
}
@@ -757,13 +766,13 @@ void Profile::activateInvalidation()
invalidation->activate(this);
}
- storeSetting("AutomaticArchiveInvalidation", true);
+ storeSetting("", "AutomaticArchiveInvalidation", true);
}
bool Profile::localSavesEnabled() const
{
- return setting("LocalSaves", false).toBool();
+ return setting("", "LocalSaves", false).toBool();
}
@@ -788,19 +797,19 @@ bool Profile::enableLocalSaves(bool enable)
return false;
}
}
- storeSetting("LocalSaves", enable);
+ storeSetting("", "LocalSaves", enable);
return true;
}
bool Profile::localSettingsEnabled() const
{
- bool enabled = setting("LocalSettings", false).toBool();
+ bool enabled = setting("", "LocalSettings", false).toBool();
if (enabled) {
QStringList missingFiles;
for (QString file : m_GamePlugin->iniFiles()) {
if (!QFile::exists(m_Directory.filePath(file))) {
- qWarning("missing %s in %s", qUtf8Printable(file), qUtf8Printable(m_Directory.path()));
+ log::warn("missing {} in {}", file, m_Directory.path());
missingFiles << file;
}
}
@@ -840,7 +849,7 @@ bool Profile::enableLocalSettings(bool enable)
return false;
}
}
- storeSetting("LocalSettings", enable);
+ storeSetting("", "LocalSettings", enable);
return true;
}
@@ -897,36 +906,41 @@ QString Profile::savePath() const
void Profile::rename(const QString &newName)
{
- QDir profileDir(Settings::instance().getProfileDirectory());
+ QDir profileDir(Settings::instance().paths().profiles());
profileDir.rename(name(), newName);
- m_Directory = profileDir.absoluteFilePath(newName);
+ m_Directory.setPath(profileDir.absoluteFilePath(newName));
+}
+
+QString keyName(const QString &section, const QString &name)
+{
+ QString key = section;
+
+ if (!name.isEmpty()) {
+ if (!key.isEmpty()) {
+ key += "/";
+ }
+
+ key += name;
+ }
+
+ return key;
}
QVariant Profile::setting(const QString &section, const QString &name,
const QVariant &fallback) const
{
- return m_Settings->value(section + "/" + name, fallback);
+ return m_Settings->value(keyName(section, name), fallback);
}
void Profile::storeSetting(const QString &section, const QString &name,
const QVariant &value)
{
- m_Settings->setValue(section + "/" + name, value);
-}
-
-void Profile::storeSetting(const QString &name, const QVariant &value)
-{
- storeSetting("", name, value);
+ m_Settings->setValue(keyName(section, name), value);
}
void Profile::removeSetting(const QString &section, const QString &name)
{
- m_Settings->remove(section + "/" + name);
-}
-
-void Profile::removeSetting(const QString &name)
-{
- removeSetting("", name);
+ m_Settings->remove(keyName(section, name));
}
QVariantMap Profile::settingsByGroup(const QString &section) const
diff --git a/src/profile.h b/src/profile.h
index bc7964f8..d0bd4a84 100644
--- a/src/profile.h
+++ b/src/profile.h
@@ -313,14 +313,14 @@ public:
void dumpModStatus() const;
- QVariant setting(const QString &section, const QString &name = QString(),
- const QVariant &fallback = QVariant()) const;
+ QVariant setting(
+ const QString &section, const QString &name,
+ const QVariant &fallback={}) const;
- void storeSetting(const QString &section, const QString &name,
- const QVariant &value);
- void storeSetting(const QString &name, const QVariant &value);
- void removeSetting(const QString &section, const QString &name = QString());
- void removeSetting(const QString &name);
+ void storeSetting(
+ const QString &section, const QString &name, const QVariant &value={});
+
+ void removeSetting(const QString &section, const QString &name);
QVariantMap settingsByGroup(const QString &section) const;
void storeSettingsByGroup(const QString &section, const QVariantMap &values);
diff --git a/src/profilesdialog.cpp b/src/profilesdialog.cpp
index f9ea655f..c91f48f4 100644
--- a/src/profilesdialog.cpp
+++ b/src/profilesdialog.cpp
@@ -57,7 +57,7 @@ ProfilesDialog::ProfilesDialog(const QString &profileName, MOBase::IPluginGame c
{
ui->setupUi(this);
- QDir profilesDir(Settings::instance().getProfileDirectory());
+ QDir profilesDir(Settings::instance().paths().profiles());
profilesDir.setFilter(QDir::AllDirs | QDir::NoDotAndDotDot);
QDirIterator profileIter(profilesDir);
@@ -84,6 +84,12 @@ ProfilesDialog::~ProfilesDialog()
delete ui;
}
+int ProfilesDialog::exec()
+{
+ GeometrySaver gs(Settings::instance(), this);
+ return QDialog::exec();
+}
+
void ProfilesDialog::showEvent(QShowEvent *event)
{
TutorableDialog::showEvent(event);
@@ -197,7 +203,7 @@ void ProfilesDialog::on_removeProfileButton_clicked()
if (confirmBox.exec() == QMessageBox::Yes) {
QString profilePath;
if (profileToDelete.get() == nullptr) {
- profilePath = Settings::instance().getProfileDirectory()
+ profilePath = Settings::instance().paths().profiles()
+ "/" + ui->profilesList->currentItem()->text();
if (QMessageBox::question(this, tr("Profile broken"),
tr("This profile you're about to delete seems to be broken or the path is invalid. "
@@ -214,9 +220,9 @@ void ProfilesDialog::on_removeProfileButton_clicked()
delete item;
}
if (!shellDelete(QStringList(profilePath))) {
- qWarning("Failed to shell-delete \"%s\" (errorcode %lu), trying regular delete", qUtf8Printable(profilePath), ::GetLastError());
+ log::warn("Failed to shell-delete \"{}\" (errorcode {}), trying regular delete", profilePath, ::GetLastError());
if (!removeDir(profilePath)) {
- qWarning("regular delete failed too");
+ log::warn("regular delete failed too");
}
}
}
diff --git a/src/profilesdialog.h b/src/profilesdialog.h
index a328ce40..a47367be 100644
--- a/src/profilesdialog.h
+++ b/src/profilesdialog.h
@@ -51,6 +51,10 @@ public:
explicit ProfilesDialog(const QString &profileName, MOBase::IPluginGame const *game, QWidget *parent = 0);
~ProfilesDialog();
+ // also saves and restores geometry
+ //
+ int exec() override;
+
/**
* @return true if creation of a new profile failed
* @todo the notion of a fail state makes little sense in the current dialog
diff --git a/src/qtgroupingproxy.cpp b/src/qtgroupingproxy.cpp
index 5fcb84d3..ff9539d7 100644
--- a/src/qtgroupingproxy.cpp
+++ b/src/qtgroupingproxy.cpp
@@ -18,11 +18,14 @@
#include "qtgroupingproxy.h"
+#include <log.h>
#include <QDebug>
#include <QIcon>
#include <QInputDialog>
+using namespace MOBase;
+
/*!
\class QtGroupingProxy
\brief The QtGroupingProxy class will group source model rows by adding a new top tree-level.
@@ -86,7 +89,6 @@ QtGroupingProxy::setGroupedColumn( int groupedColumn )
QList<RowData>
QtGroupingProxy::belongsTo( const QModelIndex &idx )
{
- //qDebug() << __FILE__ << __FUNCTION__;
QList<RowData> rowDataList;
//get all the data for this index from the model
@@ -106,7 +108,7 @@ QtGroupingProxy::belongsTo( const QModelIndex &idx )
i.next();
int role = i.key();
QVariant variant = i.value();
- // qDebug() << "role " << role << " : (" << variant.typeName() << ") : "<< variant;
+
if ( variant.type() == QVariant::List )
{
//a list of variants get's expanded to multiple rows
@@ -162,7 +164,7 @@ QtGroupingProxy::buildTree()
m_parentCreateList.clear();
int max = sourceModel()->rowCount( m_rootNode );
- //qDebug() << QString("building tree with %1 leafs.").arg( max );
+
//WARNING: these have to be added in order because the addToGroups function is optimized for
//modelRowsInserted(). Failure to do so will result in wrong data shown in the view at best.
for( int row = 0; row < max; row++ )
@@ -232,9 +234,6 @@ QtGroupingProxy::addSourceRow( const QModelIndex &idx )
int updatedGroup = -1;
if( !data.isEmpty() )
{
- // qDebug() << QString("index %1 belongs to group %2").arg( row )
- // .arg( data[0][Qt::DisplayRole].toString() );
-
foreach( const RowData &cachedData, m_groupMaps )
{
//when this matches the index belongs to an existing group
@@ -316,21 +315,12 @@ QtGroupingProxy::indexOfParentCreate( const QModelIndex &parent ) const
pc.row = parent.row();
m_parentCreateList << pc;
- //dumpParentCreateList();
- // qDebug() << QString( "m_parentCreateList: (%1)" ).arg( m_parentCreateList.size() );
- // for( int i = 0 ; i < m_parentCreateList.size() ; i++ )
- // {
- // qDebug() << i << " : " << m_parentCreateList[i].parentCreateIndex <<
- // " | " << m_parentCreateList[i].row;
- // }
-
return m_parentCreateList.size() - 1;
}
QModelIndex
QtGroupingProxy::index( int row, int column, const QModelIndex &parent ) const
{
- // qDebug() << "index requested for: (" << row << "," << column << "), " << parent;
if( !hasIndex(row, column, parent) ) {
return QModelIndex();
}
@@ -350,17 +340,15 @@ QtGroupingProxy::index( int row, int column, const QModelIndex &parent ) const
QModelIndex
QtGroupingProxy::parent( const QModelIndex &index ) const
{
- //qDebug() << "parent: " << index;
if( !index.isValid() )
return QModelIndex();
int parentCreateIndex = index.internalId();
- //qDebug() << "parentCreateIndex: " << parentCreateIndex;
if( parentCreateIndex == -1 || parentCreateIndex >= m_parentCreateList.count() )
return QModelIndex();
struct ParentCreate pc = m_parentCreateList[parentCreateIndex];
- //qDebug() << "parentCreate: (" << pc.parentCreateIndex << "," << pc.row << ")";
+
//only items at column 0 have children
return createIndex( pc.row, 0, pc.parentCreateIndex );
}
@@ -368,12 +356,10 @@ QtGroupingProxy::parent( const QModelIndex &index ) const
int
QtGroupingProxy::rowCount( const QModelIndex &index ) const
{
- //qDebug() << "rowCount: " << index;
if( !index.isValid() )
{
//the number of top level groups + the number of non-grouped items
int rows = m_groupMaps.count() + m_groupHash.value( std::numeric_limits<quint32>::max() ).count();
- //qDebug() << rows << " in root group";
return rows;
}
@@ -382,12 +368,10 @@ QtGroupingProxy::rowCount( const QModelIndex &index ) const
{
qint64 groupIndex = index.row();
int rows = m_groupHash.value( groupIndex ).count();
- //qDebug() << rows << " in group " << m_groupMaps[groupIndex];
return rows;
} else {
QModelIndex originalIndex = mapToSource( index );
int rowCount = sourceModel()->rowCount( originalIndex );
- //qDebug() << "original item: rowCount == " << rowCount;
return rowCount;
}
}
@@ -447,7 +431,7 @@ QtGroupingProxy::data( const QModelIndex &index, int role ) const
{
if( !index.isValid() )
return QVariant();
- // qDebug() << __FUNCTION__ << index << " role: " << role;
+
int row = index.row();
int column = index.column();
if( isGroup( index ) )
@@ -495,11 +479,9 @@ QtGroupingProxy::data( const QModelIndex &index, int role ) const
}
}
- //qDebug() << __FUNCTION__ << "is a group";
//use cached or precalculated data
if( m_groupMaps[row][column].contains( Qt::DisplayRole ) )
{
- // qDebug() << "Using cached data for " << row << "x" << column << ": " << m_groupMaps[row][column].value(Qt::DisplayRole).toString();
if ((m_flags & FLAG_NOGROUPNAME) != 0) {
QModelIndex parentIndex = this->index( row, 0, index.parent() );
QModelIndex childIndex = this->index( 0, column, parentIndex );
@@ -526,18 +508,17 @@ QtGroupingProxy::data( const QModelIndex &index, int role ) const
function = mapToSource(childIndex).data(m_aggregateRole).toInt();
}
- //qDebug() << __FUNCTION__ << "childCount: " << childCount;
//Need a parentIndex with column == 0 because only those have children.
QModelIndex parentIndex = this->index( row, 0, index.parent() );
for( int childRow = 0; childRow < childCount; childRow++ )
{
QModelIndex childIndex = this->index( childRow, column, parentIndex );
QVariant data = mapToSource( childIndex ).data( role );
- //qDebug() << __FUNCTION__ << data << QVariant::typeToName(data.type());
+
if( data.isValid() && !variantsOfChildren.contains( data ) )
variantsOfChildren << data;
}
- //qDebug() << "gathered this data from children: " << variantsOfChildren;
+
//saving in cache
ItemData roleMap = m_groupMaps[row].value( column );
foreach( const QVariant &variant, variantsOfChildren )
@@ -547,8 +528,6 @@ QtGroupingProxy::data( const QModelIndex &index, int role ) const
}
}
- //qDebug() << QString("roleMap[%1]:").arg(role) << roleMap[role];
-
if( variantsOfChildren.count() == 0 )
return QVariant();
@@ -621,34 +600,30 @@ QtGroupingProxy::isGroup( const QModelIndex &index ) const
QModelIndex
QtGroupingProxy::mapToSource( const QModelIndex &index ) const
{
- //qDebug() << "mapToSource: " << index;
if( !index.isValid() ) {
return m_rootNode;
}
if( isGroup( index ) )
{
- //qDebug() << "is a group: " << index.data( Qt::DisplayRole ).toString();
return m_rootNode;
}
QModelIndex proxyParent = index.parent();
- //qDebug() << "parent: " << proxyParent;
QModelIndex originalParent = mapToSource( proxyParent );
- //qDebug() << "originalParent: " << originalParent;
+
int originalRow = index.row();
if( originalParent == m_rootNode )
{
int indexInGroup = index.row();
if( !proxyParent.isValid() )
indexInGroup -= m_groupMaps.count();
- //qDebug() << "indexInGroup" << indexInGroup;
+
QList<int> childRows = m_groupHash.value( proxyParent.row() );
if( childRows.isEmpty() || indexInGroup >= childRows.count() || indexInGroup < 0 )
return QModelIndex();
originalRow = childRows.at( indexInGroup );
- //qDebug() << "originalRow: " << originalRow;
}
return sourceModel()->index( originalRow, index.column(), originalParent );
}
@@ -674,7 +649,7 @@ QtGroupingProxy::mapFromSource( const QModelIndex &idx ) const
QModelIndex proxyParent;
QModelIndex sourceParent = idx.parent();
- //qDebug() << "sourceParent: " << sourceParent;
+
int proxyRow = idx.row();
int sourceRow = idx.row();
@@ -708,15 +683,12 @@ QtGroupingProxy::mapFromSource( const QModelIndex &idx ) const
proxyParent = QModelIndex();
// if the proxy item is not in a group it will be below the groups.
int groupLength = m_groupMaps.count();
- //qDebug() << "groupNames length: " << groupLength;
int i = m_groupHash.value( std::numeric_limits<quint32>::max() ).indexOf( sourceRow );
- //qDebug() << "index in hash: " << i;
+
proxyRow = groupLength + i;
}
}
- //qDebug() << "proxyParent: " << proxyParent;
- //qDebug() << "proxyRow: " << proxyRow;
return this->index( proxyRow, idx.column(), proxyParent );
}
@@ -731,9 +703,9 @@ QtGroupingProxy::flags( const QModelIndex &idx ) const
return 0;
}
+
//only if the grouped column has the editable flag set allow the
//actions leading to setData on the source (edit & drop)
- // qDebug() << idx;
if( isGroup( idx ) )
{
// dumpGroups();
@@ -749,7 +721,7 @@ QtGroupingProxy::flags( const QModelIndex &idx ) const
m_rootNode.parent() );
if ( (originalIdx.flags() & Qt::ItemIsUserCheckable) == 0 )
{
- qDebug("row %d is not checkable", originalRow);
+ log::debug("row {} is not checkable", originalRow);
checkable = false;
}
}
@@ -892,9 +864,7 @@ QtGroupingProxy::modelRowsAboutToBeInserted( const QModelIndex &parent, int star
if( parent != m_rootNode )
{
//an item will be added to an original index, remap and pass it on
- // qDebug() << parent;
QModelIndex proxyParent = mapFromSource( parent );
- // qDebug() << proxyParent;
beginInsertRows( proxyParent, start, end );
}
}
@@ -914,7 +884,12 @@ QtGroupingProxy::modelRowsInserted( const QModelIndex &parent, int start, int en
{
//an item was added to an original index, remap and pass it on
QModelIndex proxyParent = mapFromSource( parent );
- qDebug() << proxyParent;
+
+ QString s;
+ QDebug debug(&s);
+ debug << proxyParent;
+ log::debug("{}", s);
+
//beginInsertRows had to be called in modelRowsAboutToBeInserted()
endInsertRows();
}
@@ -951,9 +926,7 @@ QtGroupingProxy::modelRowsAboutToBeRemoved( const QModelIndex &parent, int start
else
{
//child item(s) of an original item will be removed, remap and pass it on
- // qDebug() << parent;
QModelIndex proxyParent = mapFromSource( parent );
- // qDebug() << proxyParent;
beginRemoveRows( proxyParent, start, end );
}
}
@@ -1044,16 +1017,24 @@ QtGroupingProxy::isAGroupSelected( const QModelIndexList& list ) const
void
QtGroupingProxy::dumpGroups() const
{
- qDebug() << "m_groupHash: ";
+ QString s;
+ QDebug debug(&s);
+
+ debug << "m_groupHash:\n";
for( int groupIndex = -1; groupIndex < m_groupHash.keys().count() - 1; groupIndex++ )
{
- qDebug() << groupIndex << " : " << m_groupHash.value( groupIndex );
+ debug << groupIndex << " : " << m_groupHash.value( groupIndex ) << "\n";
}
- qDebug() << "m_groupMaps: ";
+ debug << "m_groupMaps:\n";
for( int groupIndex = 0; groupIndex < m_groupMaps.count(); groupIndex++ )
- qDebug() << m_groupMaps[groupIndex] << ": " << m_groupHash.value( groupIndex );
- qDebug() << m_groupHash.value( std::numeric_limits<quint32>::max() );
+ {
+ debug << m_groupMaps[groupIndex] << ": " << m_groupHash.value( groupIndex ) << "\n";
+ }
+
+ debug << m_groupHash.value( std::numeric_limits<quint32>::max() );
+
+ log::debug("{}", s);
}
diff --git a/src/resources/markdown.html b/src/resources/markdown.html
new file mode 100644
index 00000000..bd6d0f4d
--- /dev/null
+++ b/src/resources/markdown.html
@@ -0,0 +1,292 @@
+<!doctype html>
+<html lang="en">
+<meta charset="utf-8">
+<head>
+ <script>
+ /**
+ * marked - a markdown parser
+ * Copyright (c) 2011-2018, Christopher Jeffrey. (MIT Licensed)
+ * https://github.com/markedjs/marked
+ */
+ !function(e){"use strict";var x={newline:/^\n+/,code:/^( {4}[^\n]+\n*)+/,fences:/^ {0,3}(`{3,}|~{3,})([^`~\n]*)\n(?:|([\s\S]*?)\n)(?: {0,3}\1[~`]* *(?:\n+|$)|$)/,hr:/^ {0,3}((?:- *){3,}|(?:_ *){3,}|(?:\* *){3,})(?:\n+|$)/,heading:/^ {0,3}(#{1,6}) +([^\n]*?)(?: +#+)? *(?:\n+|$)/,blockquote:/^( {0,3}> ?(paragraph|[^\n]*)(?:\n|$))+/,list:/^( {0,3})(bull) [\s\S]+?(?:hr|def|\n{2,}(?! )(?!\1bull )\n*|\s*$)/,html:"^ {0,3}(?:<(script|pre|style)[\\s>][\\s\\S]*?(?:</\\1>[^\\n]*\\n+|$)|comment[^\\n]*(\\n+|$)|<\\?[\\s\\S]*?\\?>\\n*|<![A-Z][\\s\\S]*?>\\n*|<!\\[CDATA\\[[\\s\\S]*?\\]\\]>\\n*|</?(tag)(?: +|\\n|/?>)[\\s\\S]*?(?:\\n{2,}|$)|<(?!script|pre|style)([a-z][\\w-]*)(?:attribute)*? */?>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:\\n{2,}|$)|</(?!script|pre|style)[a-z][\\w-]*\\s*>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:\\n{2,}|$))",def:/^ {0,3}\[(label)\]: *\n? *<?([^\s>]+)>?(?:(?: +\n? *| *\n *)(title))? *(?:\n+|$)/,nptable:g,table:g,lheading:/^([^\n]+)\n {0,3}(=+|-+) *(?:\n+|$)/,_paragraph:/^([^\n]+(?:\n(?!hr|heading|lheading|blockquote|fences|list|html)[^\n]+)*)/,text:/^[^\n]+/};function a(e){this.tokens=[],this.tokens.links=Object.create(null),this.options=e||k.defaults,this.rules=x.normal,this.options.pedantic?this.rules=x.pedantic:this.options.gfm&&(this.rules=x.gfm)}x._label=/(?!\s*\])(?:\\[\[\]]|[^\[\]])+/,x._title=/(?:"(?:\\"?|[^"\\])*"|'[^'\n]*(?:\n[^'\n]+)*\n?'|\([^()]*\))/,x.def=i(x.def).replace("label",x._label).replace("title",x._title).getRegex(),x.bullet=/(?:[*+-]|\d{1,9}\.)/,x.item=/^( *)(bull) ?[^\n]*(?:\n(?!\1bull ?)[^\n]*)*/,x.item=i(x.item,"gm").replace(/bull/g,x.bullet).getRegex(),x.list=i(x.list).replace(/bull/g,x.bullet).replace("hr","\\n+(?=\\1?(?:(?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})(?:\\n+|$))").replace("def","\\n+(?="+x.def.source+")").getRegex(),x._tag="address|article|aside|base|basefont|blockquote|body|caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|frame|frameset|h[1-6]|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem|meta|nav|noframes|ol|optgroup|option|p|param|section|source|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul",x._comment=/<!--(?!-?>)[\s\S]*?-->/,x.html=i(x.html,"i").replace("comment",x._comment).replace("tag",x._tag).replace("attribute",/ +[a-zA-Z:_][\w.:-]*(?: *= *"[^"\n]*"| *= *'[^'\n]*'| *= *[^\s"'=<>`]+)?/).getRegex(),x.paragraph=i(x._paragraph).replace("hr",x.hr).replace("heading"," {0,3}#{1,6} +").replace("|lheading","").replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}|~{3,})[^`\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html","</?(?:tag)(?: +|\\n|/?>)|<(?:script|pre|style|!--)").replace("tag",x._tag).getRegex(),x.blockquote=i(x.blockquote).replace("paragraph",x.paragraph).getRegex(),x.normal=f({},x),x.gfm=f({},x.normal,{nptable:/^ *([^|\n ].*\|.*)\n *([-:]+ *\|[-| :]*)(?:\n((?:.*[^>\n ].*(?:\n|$))*)\n*|$)/,table:/^ *\|(.+)\n *\|?( *[-:]+[-| :]*)(?:\n((?: *[^>\n ].*(?:\n|$))*)\n*|$)/}),x.pedantic=f({},x.normal,{html:i("^ *(?:comment *(?:\\n|\\s*$)|<(tag)[\\s\\S]+?</\\1> *(?:\\n{2,}|\\s*$)|<tag(?:\"[^\"]*\"|'[^']*'|\\s[^'\"/>\\s]*)*?/?> *(?:\\n{2,}|\\s*$))").replace("comment",x._comment).replace(/tag/g,"(?!(?:a|em|strong|small|s|cite|q|dfn|abbr|data|time|code|var|samp|kbd|sub|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo|span|br|wbr|ins|del|img)\\b)\\w+(?!:|[^\\w\\s@]*@)\\b").getRegex(),def:/^ *\[([^\]]+)\]: *<?([^\s>]+)>?(?: +(["(][^\n]+[")]))? *(?:\n+|$)/,heading:/^ *(#{1,6}) *([^\n]+?) *(?:#+ *)?(?:\n+|$)/,fences:g,paragraph:i(x.normal._paragraph).replace("hr",x.hr).replace("heading"," *#{1,6} *[^\n]").replace("lheading",x.lheading).replace("blockquote"," {0,3}>").replace("|fences","").replace("|list","").replace("|html","").getRegex()}),a.rules=x,a.lex=function(e,t){return new a(t).lex(e)},a.prototype.lex=function(e){return e=e.replace(/\r\n|\r/g,"\n").replace(/\t/g," ").replace(/\u00a0/g," ").replace(/\u2424/g,"\n"),this.token(e,!0)},a.prototype.token=function(e,t){var n,r,s,i,l,o,a,h,p,u,c,g,f,d,m,k;for(e=e.replace(/^ +$/gm,"");e;)if((s=this.rules.newline.exec(e))&&(e=e.substring(s[0].length),1<s[0].length&&this.tokens.push({type:"space"})),s=this.rules.code.exec(e)){var b=this.tokens[this.tokens.length-1];e=e.substring(s[0].length),b&&"paragraph"===b.type?b.text+="\n"+s[0].trimRight():(s=s[0].replace(/^ {4}/gm,""),this.tokens.push({type:"code",codeBlockStyle:"indented",text:this.options.pedantic?s:w(s,"\n")}))}else if(s=this.rules.fences.exec(e))e=e.substring(s[0].length),this.tokens.push({type:"code",lang:s[2]?s[2].trim():s[2],text:s[3]||""});else if(s=this.rules.heading.exec(e))e=e.substring(s[0].length),this.tokens.push({type:"heading",depth:s[1].length,text:s[2]});else if((s=this.rules.nptable.exec(e))&&(o={type:"table",header:y(s[1].replace(/^ *| *\| *$/g,"")),align:s[2].replace(/^ *|\| *$/g,"").split(/ *\| */),cells:s[3]?s[3].replace(/\n$/,"").split("\n"):[]}).header.length===o.align.length){for(e=e.substring(s[0].length),c=0;c<o.align.length;c++)/^ *-+: *$/.test(o.align[c])?o.align[c]="right":/^ *:-+: *$/.test(o.align[c])?o.align[c]="center":/^ *:-+ *$/.test(o.align[c])?o.align[c]="left":o.align[c]=null;for(c=0;c<o.cells.length;c++)o.cells[c]=y(o.cells[c],o.header.length);this.tokens.push(o)}else if(s=this.rules.hr.exec(e))e=e.substring(s[0].length),this.tokens.push({type:"hr"});else if(s=this.rules.blockquote.exec(e))e=e.substring(s[0].length),this.tokens.push({type:"blockquote_start"}),s=s[0].replace(/^ *> ?/gm,""),this.token(s,t),this.tokens.push({type:"blockquote_end"});else if(s=this.rules.list.exec(e)){for(e=e.substring(s[0].length),a={type:"list_start",ordered:d=1<(i=s[2]).length,start:d?+i:"",loose:!1},this.tokens.push(a),n=!(h=[]),f=(s=s[0].match(this.rules.item)).length,c=0;c<f;c++)u=(o=s[c]).length,~(o=o.replace(/^ *([*+-]|\d+\.) */,"")).indexOf("\n ")&&(u-=o.length,o=this.options.pedantic?o.replace(/^ {1,4}/gm,""):o.replace(new RegExp("^ {1,"+u+"}","gm"),"")),c!==f-1&&(l=x.bullet.exec(s[c+1])[0],(1<i.length?1===l.length:1<l.length||this.options.smartLists&&l!==i)&&(e=s.slice(c+1).join("\n")+e,c=f-1)),r=n||/\n\n(?!\s*$)/.test(o),c!==f-1&&(n="\n"===o.charAt(o.length-1),r||(r=n)),r&&(a.loose=!0),k=void 0,(m=/^\[[ xX]\] /.test(o))&&(k=" "!==o[1],o=o.replace(/^\[[ xX]\] +/,"")),p={type:"list_item_start",task:m,checked:k,loose:r},h.push(p),this.tokens.push(p),this.token(o,!1),this.tokens.push({type:"list_item_end"});if(a.loose)for(f=h.length,c=0;c<f;c++)h[c].loose=!0;this.tokens.push({type:"list_end"})}else if(s=this.rules.html.exec(e))e=e.substring(s[0].length),this.tokens.push({type:this.options.sanitize?"paragraph":"html",pre:!this.options.sanitizer&&("pre"===s[1]||"script"===s[1]||"style"===s[1]),text:this.options.sanitize?this.options.sanitizer?this.options.sanitizer(s[0]):_(s[0]):s[0]});else if(t&&(s=this.rules.def.exec(e)))e=e.substring(s[0].length),s[3]&&(s[3]=s[3].substring(1,s[3].length-1)),g=s[1].toLowerCase().replace(/\s+/g," "),this.tokens.links[g]||(this.tokens.links[g]={href:s[2],title:s[3]});else if((s=this.rules.table.exec(e))&&(o={type:"table",header:y(s[1].replace(/^ *| *\| *$/g,"")),align:s[2].replace(/^ *|\| *$/g,"").split(/ *\| */),cells:s[3]?s[3].replace(/\n$/,"").split("\n"):[]}).header.length===o.align.length){for(e=e.substring(s[0].length),c=0;c<o.align.length;c++)/^ *-+: *$/.test(o.align[c])?o.align[c]="right":/^ *:-+: *$/.test(o.align[c])?o.align[c]="center":/^ *:-+ *$/.test(o.align[c])?o.align[c]="left":o.align[c]=null;for(c=0;c<o.cells.length;c++)o.cells[c]=y(o.cells[c].replace(/^ *\| *| *\| *$/g,""),o.header.length);this.tokens.push(o)}else if(s=this.rules.lheading.exec(e))e=e.substring(s[0].length),this.tokens.push({type:"heading",depth:"="===s[2].charAt(0)?1:2,text:s[1]});else if(t&&(s=this.rules.paragraph.exec(e)))e=e.substring(s[0].length),this.tokens.push({type:"paragraph",text:"\n"===s[1].charAt(s[1].length-1)?s[1].slice(0,-1):s[1]});else if(s=this.rules.text.exec(e))e=e.substring(s[0].length),this.tokens.push({type:"text",text:s[0]});else if(e)throw new Error("Infinite loop on byte: "+e.charCodeAt(0));return this.tokens};var n={escape:/^\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/,autolink:/^<(scheme:[^\s\x00-\x1f<>]*|email)>/,url:g,tag:"^comment|^</[a-zA-Z][\\w:-]*\\s*>|^<[a-zA-Z][\\w-]*(?:attribute)*?\\s*/?>|^<\\?[\\s\\S]*?\\?>|^<![a-zA-Z]+\\s[\\s\\S]*?>|^<!\\[CDATA\\[[\\s\\S]*?\\]\\]>",link:/^!?\[(label)\]\(\s*(href)(?:\s+(title))?\s*\)/,reflink:/^!?\[(label)\]\[(?!\s*\])((?:\\[\[\]]?|[^\[\]\\])+)\]/,nolink:/^!?\[(?!\s*\])((?:\[[^\[\]]*\]|\\[\[\]]|[^\[\]])*)\](?:\[\])?/,strong:/^__([^\s_])__(?!_)|^\*\*([^\s*])\*\*(?!\*)|^__([^\s][\s\S]*?[^\s])__(?!_)|^\*\*([^\s][\s\S]*?[^\s])\*\*(?!\*)/,em:/^_([^\s_])_(?!_)|^\*([^\s*<\[])\*(?!\*)|^_([^\s<][\s\S]*?[^\s_])_(?!_|[^\spunctuation])|^_([^\s_<][\s\S]*?[^\s])_(?!_|[^\spunctuation])|^\*([^\s<"][\s\S]*?[^\s\*])\*(?!\*|[^\spunctuation])|^\*([^\s*"<\[][\s\S]*?[^\s])\*(?!\*)/,code:/^(`+)([^`]|[^`][\s\S]*?[^`])\1(?!`)/,br:/^( {2,}|\\)\n(?!\s*$)/,del:g,text:/^(`+|[^`])(?:[\s\S]*?(?:(?=[\\<!\[`*]|\b_|$)|[^ ](?= {2,}\n))|(?= {2,}\n))/};function p(e,t){if(this.options=t||k.defaults,this.links=e,this.rules=n.normal,this.renderer=this.options.renderer||new r,this.renderer.options=this.options,!this.links)throw new Error("Tokens array requires a `links` property.");this.options.pedantic?this.rules=n.pedantic:this.options.gfm&&(this.options.breaks?this.rules=n.breaks:this.rules=n.gfm)}function r(e){this.options=e||k.defaults}function s(){}function h(e){this.tokens=[],this.token=null,this.options=e||k.defaults,this.options.renderer=this.options.renderer||new r,this.renderer=this.options.renderer,this.renderer.options=this.options,this.slugger=new t}function t(){this.seen={}}function _(e,t){if(t){if(_.escapeTest.test(e))return e.replace(_.escapeReplace,function(e){return _.replacements[e]})}else if(_.escapeTestNoEncode.test(e))return e.replace(_.escapeReplaceNoEncode,function(e){return _.replacements[e]});return e}function c(e){return e.replace(/&(#(?:\d+)|(?:#x[0-9A-Fa-f]+)|(?:\w+));?/gi,function(e,t){return"colon"===(t=t.toLowerCase())?":":"#"===t.charAt(0)?"x"===t.charAt(1)?String.fromCharCode(parseInt(t.substring(2),16)):String.fromCharCode(+t.substring(1)):""})}function i(n,e){return n=n.source||n,e=e||"",{replace:function(e,t){return t=(t=t.source||t).replace(/(^|[^\[])\^/g,"$1"),n=n.replace(e,t),this},getRegex:function(){return new RegExp(n,e)}}}function l(e,t,n){if(e){try{var r=decodeURIComponent(c(n)).replace(/[^\w:]/g,"").toLowerCase()}catch(e){return null}if(0===r.indexOf("javascript:")||0===r.indexOf("vbscript:")||0===r.indexOf("data:"))return null}t&&!u.test(n)&&(n=function(e,t){o[" "+e]||(/^[^:]+:\/*[^/]*$/.test(e)?o[" "+e]=e+"/":o[" "+e]=w(e,"/",!0));return e=o[" "+e],"//"===t.slice(0,2)?e.replace(/:[\s\S]*/,":")+t:"/"===t.charAt(0)?e.replace(/(:\/*[^/]*)[\s\S]*/,"$1")+t:e+t}(t,n));try{n=encodeURI(n).replace(/%25/g,"%")}catch(e){return null}return n}n._punctuation="!\"#$%&'()*+,\\-./:;<=>?@\\[^_{|}~",n.em=i(n.em).replace(/punctuation/g,n._punctuation).getRegex(),n._escapes=/\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/g,n._scheme=/[a-zA-Z][a-zA-Z0-9+.-]{1,31}/,n._email=/[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+(@)[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(?![-_])/,n.autolink=i(n.autolink).replace("scheme",n._scheme).replace("email",n._email).getRegex(),n._attribute=/\s+[a-zA-Z:_][\w.:-]*(?:\s*=\s*"[^"]*"|\s*=\s*'[^']*'|\s*=\s*[^\s"'=<>`]+)?/,n.tag=i(n.tag).replace("comment",x._comment).replace("attribute",n._attribute).getRegex(),n._label=/(?:\[[^\[\]]*\]|\\.|`[^`]*`|[^\[\]\\`])*?/,n._href=/<(?:\\[<>]?|[^\s<>\\])*>|[^\s\x00-\x1f]*/,n._title=/"(?:\\"?|[^"\\])*"|'(?:\\'?|[^'\\])*'|\((?:\\\)?|[^)\\])*\)/,n.link=i(n.link).replace("label",n._label).replace("href",n._href).replace("title",n._title).getRegex(),n.reflink=i(n.reflink).replace("label",n._label).getRegex(),n.normal=f({},n),n.pedantic=f({},n.normal,{strong:/^__(?=\S)([\s\S]*?\S)__(?!_)|^\*\*(?=\S)([\s\S]*?\S)\*\*(?!\*)/,em:/^_(?=\S)([\s\S]*?\S)_(?!_)|^\*(?=\S)([\s\S]*?\S)\*(?!\*)/,link:i(/^!?\[(label)\]\((.*?)\)/).replace("label",n._label).getRegex(),reflink:i(/^!?\[(label)\]\s*\[([^\]]*)\]/).replace("label",n._label).getRegex()}),n.gfm=f({},n.normal,{escape:i(n.escape).replace("])","~|])").getRegex(),_extended_email:/[A-Za-z0-9._+-]+(@)[a-zA-Z0-9-_]+(?:\.[a-zA-Z0-9-_]*[a-zA-Z0-9])+(?![-_])/,url:/^((?:ftp|https?):\/\/|www\.)(?:[a-zA-Z0-9\-]+\.?)+[^\s<]*|^email/,_backpedal:/(?:[^?!.,:;*_~()&]+|\([^)]*\)|&(?![a-zA-Z0-9]+;$)|[?!.,:;*_~)]+(?!$))+/,del:/^~+(?=\S)([\s\S]*?\S)~+/,text:/^(`+|[^`])(?:[\s\S]*?(?:(?=[\\<!\[`*~]|\b_|https?:\/\/|ftp:\/\/|www\.|$)|[^ ](?= {2,}\n)|[^a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-](?=[a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-]+@))|(?= {2,}\n|[a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-]+@))/}),n.gfm.url=i(n.gfm.url,"i").replace("email",n.gfm._extended_email).getRegex(),n.breaks=f({},n.gfm,{br:i(n.br).replace("{2,}","*").getRegex(),text:i(n.gfm.text).replace("\\b_","\\b_| {2,}\\n").replace(/\{2,\}/g,"*").getRegex()}),p.rules=n,p.output=function(e,t,n){return new p(t,n).output(e)},p.prototype.output=function(e){for(var t,n,r,s,i,l,o="";e;)if(i=this.rules.escape.exec(e))e=e.substring(i[0].length),o+=_(i[1]);else if(i=this.rules.tag.exec(e))!this.inLink&&/^<a /i.test(i[0])?this.inLink=!0:this.inLink&&/^<\/a>/i.test(i[0])&&(this.inLink=!1),!this.inRawBlock&&/^<(pre|code|kbd|script)(\s|>)/i.test(i[0])?this.inRawBlock=!0:this.inRawBlock&&/^<\/(pre|code|kbd|script)(\s|>)/i.test(i[0])&&(this.inRawBlock=!1),e=e.substring(i[0].length),o+=this.options.sanitize?this.options.sanitizer?this.options.sanitizer(i[0]):_(i[0]):i[0];else if(i=this.rules.link.exec(e)){var a=d(i[2],"()");if(-1<a){var h=4+i[1].length+a;i[2]=i[2].substring(0,a),i[0]=i[0].substring(0,h).trim(),i[3]=""}e=e.substring(i[0].length),this.inLink=!0,r=i[2],s=this.options.pedantic?(t=/^([^'"]*[^\s])\s+(['"])(.*)\2/.exec(r))?(r=t[1],t[3]):"":i[3]?i[3].slice(1,-1):"",r=r.trim().replace(/^<([\s\S]*)>$/,"$1"),o+=this.outputLink(i,{href:p.escapes(r),title:p.escapes(s)}),this.inLink=!1}else if((i=this.rules.reflink.exec(e))||(i=this.rules.nolink.exec(e))){if(e=e.substring(i[0].length),t=(i[2]||i[1]).replace(/\s+/g," "),!(t=this.links[t.toLowerCase()])||!t.href){o+=i[0].charAt(0),e=i[0].substring(1)+e;continue}this.inLink=!0,o+=this.outputLink(i,t),this.inLink=!1}else if(i=this.rules.strong.exec(e))e=e.substring(i[0].length),o+=this.renderer.strong(this.output(i[4]||i[3]||i[2]||i[1]));else if(i=this.rules.em.exec(e))e=e.substring(i[0].length),o+=this.renderer.em(this.output(i[6]||i[5]||i[4]||i[3]||i[2]||i[1]));else if(i=this.rules.code.exec(e))e=e.substring(i[0].length),o+=this.renderer.codespan(_(i[2].trim(),!0));else if(i=this.rules.br.exec(e))e=e.substring(i[0].length),o+=this.renderer.br();else if(i=this.rules.del.exec(e))e=e.substring(i[0].length),o+=this.renderer.del(this.output(i[1]));else if(i=this.rules.autolink.exec(e))e=e.substring(i[0].length),r="@"===i[2]?"mailto:"+(n=_(this.mangle(i[1]))):n=_(i[1]),o+=this.renderer.link(r,null,n);else if(this.inLink||!(i=this.rules.url.exec(e))){if(i=this.rules.text.exec(e))e=e.substring(i[0].length),this.inRawBlock?o+=this.renderer.text(this.options.sanitize?this.options.sanitizer?this.options.sanitizer(i[0]):_(i[0]):i[0]):o+=this.renderer.text(_(this.smartypants(i[0])));else if(e)throw new Error("Infinite loop on byte: "+e.charCodeAt(0))}else{if("@"===i[2])r="mailto:"+(n=_(i[0]));else{for(;l=i[0],i[0]=this.rules._backpedal.exec(i[0])[0],l!==i[0];);n=_(i[0]),r="www."===i[1]?"http://"+n:n}e=e.substring(i[0].length),o+=this.renderer.link(r,null,n)}return o},p.escapes=function(e){return e?e.replace(p.rules._escapes,"$1"):e},p.prototype.outputLink=function(e,t){var n=t.href,r=t.title?_(t.title):null;return"!"!==e[0].charAt(0)?this.renderer.link(n,r,this.output(e[1])):this.renderer.image(n,r,_(e[1]))},p.prototype.smartypants=function(e){return this.options.smartypants?e.replace(/---/g,"—").replace(/--/g,"–").replace(/(^|[-\u2014/(\[{"\s])'/g,"$1‘").replace(/'/g,"’").replace(/(^|[-\u2014/(\[{\u2018\s])"/g,"$1“").replace(/"/g,"”").replace(/\.{3}/g,"…"):e},p.prototype.mangle=function(e){if(!this.options.mangle)return e;for(var t,n="",r=e.length,s=0;s<r;s++)t=e.charCodeAt(s),.5<Math.random()&&(t="x"+t.toString(16)),n+="&#"+t+";";return n},r.prototype.code=function(e,t,n){var r=(t||"").match(/\S*/)[0];if(this.options.highlight){var s=this.options.highlight(e,r);null!=s&&s!==e&&(n=!0,e=s)}return r?'<pre><code class="'+this.options.langPrefix+_(r,!0)+'">'+(n?e:_(e,!0))+"</code></pre>\n":"<pre><code>"+(n?e:_(e,!0))+"</code></pre>"},r.prototype.blockquote=function(e){return"<blockquote>\n"+e+"</blockquote>\n"},r.prototype.html=function(e){return e},r.prototype.heading=function(e,t,n,r){return this.options.headerIds?"<h"+t+' id="'+this.options.headerPrefix+r.slug(n)+'">'+e+"</h"+t+">\n":"<h"+t+">"+e+"</h"+t+">\n"},r.prototype.hr=function(){return this.options.xhtml?"<hr/>\n":"<hr>\n"},r.prototype.list=function(e,t,n){var r=t?"ol":"ul";return"<"+r+(t&&1!==n?' start="'+n+'"':"")+">\n"+e+"</"+r+">\n"},r.prototype.listitem=function(e){return"<li>"+e+"</li>\n"},r.prototype.checkbox=function(e){return"<input "+(e?'checked="" ':"")+'disabled="" type="checkbox"'+(this.options.xhtml?" /":"")+"> "},r.prototype.paragraph=function(e){return"<p>"+e+"</p>\n"},r.prototype.table=function(e,t){return t&&(t="<tbody>"+t+"</tbody>"),"<table>\n<thead>\n"+e+"</thead>\n"+t+"</table>\n"},r.prototype.tablerow=function(e){return"<tr>\n"+e+"</tr>\n"},r.prototype.tablecell=function(e,t){var n=t.header?"th":"td";return(t.align?"<"+n+' align="'+t.align+'">':"<"+n+">")+e+"</"+n+">\n"},r.prototype.strong=function(e){return"<strong>"+e+"</strong>"},r.prototype.em=function(e){return"<em>"+e+"</em>"},r.prototype.codespan=function(e){return"<code>"+e+"</code>"},r.prototype.br=function(){return this.options.xhtml?"<br/>":"<br>"},r.prototype.del=function(e){return"<del>"+e+"</del>"},r.prototype.link=function(e,t,n){if(null===(e=l(this.options.sanitize,this.options.baseUrl,e)))return n;var r='<a href="'+_(e)+'"';return t&&(r+=' title="'+t+'"'),r+=">"+n+"</a>"},r.prototype.image=function(e,t,n){if(null===(e=l(this.options.sanitize,this.options.baseUrl,e)))return n;var r='<img src="'+e+'" alt="'+n+'"';return t&&(r+=' title="'+t+'"'),r+=this.options.xhtml?"/>":">"},r.prototype.text=function(e){return e},s.prototype.strong=s.prototype.em=s.prototype.codespan=s.prototype.del=s.prototype.text=function(e){return e},s.prototype.link=s.prototype.image=function(e,t,n){return""+n},s.prototype.br=function(){return""},h.parse=function(e,t){return new h(t).parse(e)},h.prototype.parse=function(e){this.inline=new p(e.links,this.options),this.inlineText=new p(e.links,f({},this.options,{renderer:new s})),this.tokens=e.reverse();for(var t="";this.next();)t+=this.tok();return t},h.prototype.next=function(){return this.token=this.tokens.pop(),this.token},h.prototype.peek=function(){return this.tokens[this.tokens.length-1]||0},h.prototype.parseText=function(){for(var e=this.token.text;"text"===this.peek().type;)e+="\n"+this.next().text;return this.inline.output(e)},h.prototype.tok=function(){switch(this.token.type){case"space":return"";case"hr":return this.renderer.hr();case"heading":return this.renderer.heading(this.inline.output(this.token.text),this.token.depth,c(this.inlineText.output(this.token.text)),this.slugger);case"code":return this.renderer.code(this.token.text,this.token.lang,this.token.escaped);case"table":var e,t,n,r,s="",i="";for(n="",e=0;e<this.token.header.length;e++)n+=this.renderer.tablecell(this.inline.output(this.token.header[e]),{header:!0,align:this.token.align[e]});for(s+=this.renderer.tablerow(n),e=0;e<this.token.cells.length;e++){for(t=this.token.cells[e],n="",r=0;r<t.length;r++)n+=this.renderer.tablecell(this.inline.output(t[r]),{header:!1,align:this.token.align[r]});i+=this.renderer.tablerow(n)}return this.renderer.table(s,i);case"blockquote_start":for(i="";"blockquote_end"!==this.next().type;)i+=this.tok();return this.renderer.blockquote(i);case"list_start":i="";for(var l=this.token.ordered,o=this.token.start;"list_end"!==this.next().type;)i+=this.tok();return this.renderer.list(i,l,o);case"list_item_start":i="";var a=this.token.loose,h=this.token.checked,p=this.token.task;for(this.token.task&&(i+=this.renderer.checkbox(h));"list_item_end"!==this.next().type;)i+=a||"text"!==this.token.type?this.tok():this.parseText();return this.renderer.listitem(i,p,h);case"html":return this.renderer.html(this.token.text);case"paragraph":return this.renderer.paragraph(this.inline.output(this.token.text));case"text":return this.renderer.paragraph(this.parseText());default:var u='Token with "'+this.token.type+'" type was not found.';if(!this.options.silent)throw new Error(u);console.log(u)}},t.prototype.slug=function(e){var t=e.toLowerCase().trim().replace(/[\u2000-\u206F\u2E00-\u2E7F\\'!"#$%&()*+,./:;<=>?@[\]^`{|}~]/g,"").replace(/\s/g,"-");if(this.seen.hasOwnProperty(t))for(var n=t;this.seen[n]++,t=n+"-"+this.seen[n],this.seen.hasOwnProperty(t););return this.seen[t]=0,t},_.escapeTest=/[&<>"']/,_.escapeReplace=/[&<>"']/g,_.replacements={"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"},_.escapeTestNoEncode=/[<>"']|&(?!#?\w+;)/,_.escapeReplaceNoEncode=/[<>"']|&(?!#?\w+;)/g;var o={},u=/^$|^[a-z][a-z0-9+.-]*:|^[?#]/i;function g(){}function f(e){for(var t,n,r=1;r<arguments.length;r++)for(n in t=arguments[r])Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n]);return e}function y(e,t){var n=e.replace(/\|/g,function(e,t,n){for(var r=!1,s=t;0<=--s&&"\\"===n[s];)r=!r;return r?"|":" |"}).split(/ \|/),r=0;if(n.length>t)n.splice(t);else for(;n.length<t;)n.push("");for(;r<n.length;r++)n[r]=n[r].trim().replace(/\\\|/g,"|");return n}function w(e,t,n){if(0===e.length)return"";for(var r=0;r<e.length;){var s=e.charAt(e.length-r-1);if(s!==t||n){if(s===t||!n)break;r++}else r++}return e.substr(0,e.length-r)}function d(e,t){if(-1===e.indexOf(t[1]))return-1;for(var n=0,r=0;r<e.length;r++)if("\\"===e[r])r++;else if(e[r]===t[0])n++;else if(e[r]===t[1]&&--n<0)return r;return-1}function m(e){e&&e.sanitize&&!e.silent&&console.warn("marked(): sanitize and sanitizer parameters are deprecated since version 0.7.0, should not be used and will be removed in the future. Read more here: https://marked.js.org/#/USING_ADVANCED.md#options")}function k(e,n,r){if(null==e)throw new Error("marked(): input parameter is undefined or null");if("string"!=typeof e)throw new Error("marked(): input parameter is of type "+Object.prototype.toString.call(e)+", string expected");if(r||"function"==typeof n){r||(r=n,n=null),m(n=f({},k.defaults,n||{}));var s,i,l=n.highlight,t=0;try{s=a.lex(e,n)}catch(e){return r(e)}i=s.length;var o=function(t){if(t)return n.highlight=l,r(t);var e;try{e=h.parse(s,n)}catch(e){t=e}return n.highlight=l,t?r(t):r(null,e)};if(!l||l.length<3)return o();if(delete n.highlight,!i)return o();for(;t<s.length;t++)!function(n){"code"!==n.type?--i||o():l(n.text,n.lang,function(e,t){return e?o(e):null==t||t===n.text?--i||o():(n.text=t,n.escaped=!0,void(--i||o()))})}(s[t])}else try{return n&&(n=f({},k.defaults,n)),m(n),h.parse(a.lex(e,n),n)}catch(e){if(e.message+="\nPlease report this to https://github.com/markedjs/marked.",(n||k.defaults).silent)return"<p>An error occurred:</p><pre>"+_(e.message+"",!0)+"</pre>";throw e}}g.exec=g,k.options=k.setOptions=function(e){return f(k.defaults,e),k},k.getDefaults=function(){return{baseUrl:null,breaks:!1,gfm:!0,headerIds:!0,headerPrefix:"",highlight:null,langPrefix:"language-",mangle:!0,pedantic:!1,renderer:new r,sanitize:!1,sanitizer:null,silent:!1,smartLists:!1,smartypants:!1,xhtml:!1}},k.defaults=k.getDefaults(),k.Parser=h,k.parser=h.parse,k.Renderer=r,k.TextRenderer=s,k.Lexer=a,k.lexer=a.lex,k.InlineLexer=p,k.inlineLexer=p.output,k.Slugger=t,k.parse=k,"undefined"!=typeof module&&"object"==typeof exports?module.exports=k:"function"==typeof define&&define.amd?define(function(){return k}):e.marked=k}(this||("undefined"!=typeof window?window:global));
+ </script>
+ <script src="qrc:/qtwebchannel/qwebchannel.js"></script>
+ <style>
+ body{
+ margin: 0 auto;
+ background: #2D2D30;
+ font-family: Sans-serif;
+ color: #F1F1F1;
+ line-height: 1;
+ padding-left: 10px;
+ padding-top: 0px;
+ }
+ h1, h2, h3, h4 {
+ font-weight: 400;
+ }
+ h1, h2, h3, h4, h5, p {
+ margin-bottom: 24px;
+ padding: 0;
+ }
+ h1 {
+ font-size: 48px;
+ }
+ h2 {
+ font-size: 36px;
+ margin: 24px 0 6px;
+ }
+ h3 {
+ font-size: 24px;
+ }
+ h4 {
+ font-size: 21px;
+ }
+ h5 {
+ font-size: 18px;
+ }
+ a {
+ color: #61BFC1;
+ margin: 0;
+ padding: 0;
+ text-decoration: none;
+ vertical-align: baseline;
+ }
+ a:hover {
+ text-decoration: underline;
+ }
+ a:visited {
+ color: #466B6C;
+ }
+ ul, ol {
+ padding: 0;
+ padding-left: 20px;
+ margin: 0;
+ }
+ li {
+ line-height: 24px;
+ }
+ li ul, li ul {
+ margin-left: 24px;
+ }
+ p, ul, ol {
+ font-size: 14px;
+ line-height: 24px;
+ }
+ pre {
+ padding: 0px 24px;
+ white-space: pre-wrap;
+ }
+ code {
+ font-family: Consolas, Monaco, Andale Mono, monospace;
+ line-height: 1.5;
+ font-size: 13px;
+ }
+ aside {
+ display: block;
+ float: right;
+ width: 390px;
+ }
+ blockquote {
+ border-left:.5em solid #eee;
+ padding: 0 2em;
+ margin-left:0;
+ }
+ blockquote cite {
+ font-size:14px;
+ line-height:20px;
+ color:#bfbfbf;
+ }
+ blockquote cite:before {
+ content: '\2014 \00A0';
+ }
+
+ blockquote p {
+ color: #666;
+ }
+ hr {
+ width: 540px;
+ text-align: left;
+ margin: 0 auto 0 0;
+ color: #999;
+ }
+
+ /* Code below this line is copyright Twitter Inc. */
+
+ button,
+ input,
+ select,
+ textarea {
+ font-size: 100%;
+ margin: 0;
+ vertical-align: baseline;
+ *vertical-align: middle;
+ }
+ button, input {
+ line-height: normal;
+ *overflow: visible;
+ }
+ button::-moz-focus-inner, input::-moz-focus-inner {
+ border: 0;
+ padding: 0;
+ }
+ button,
+ input[type="button"],
+ input[type="reset"],
+ input[type="submit"] {
+ cursor: pointer;
+ -webkit-appearance: button;
+ }
+ input[type=checkbox], input[type=radio] {
+ cursor: pointer;
+ }
+ /* override default chrome & firefox settings */
+ input:not([type="image"]), textarea {
+ -webkit-box-sizing: content-box;
+ -moz-box-sizing: content-box;
+ box-sizing: content-box;
+ }
+
+ input[type="search"] {
+ -webkit-appearance: textfield;
+ -webkit-box-sizing: content-box;
+ -moz-box-sizing: content-box;
+ box-sizing: content-box;
+ }
+ input[type="search"]::-webkit-search-decoration {
+ -webkit-appearance: none;
+ }
+ label,
+ input,
+ select,
+ textarea {
+ font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+ font-size: 13px;
+ font-weight: normal;
+ line-height: normal;
+ margin-bottom: 18px;
+ }
+ input[type=checkbox], input[type=radio] {
+ cursor: pointer;
+ margin-bottom: 0;
+ }
+ input[type=text],
+ input[type=password],
+ textarea,
+ select {
+ display: inline-block;
+ width: 210px;
+ padding: 4px;
+ font-size: 13px;
+ font-weight: normal;
+ line-height: 18px;
+ height: 18px;
+ color: #808080;
+ border: 1px solid #ccc;
+ -webkit-border-radius: 3px;
+ -moz-border-radius: 3px;
+ border-radius: 3px;
+ }
+ select, input[type=file] {
+ height: 27px;
+ line-height: 27px;
+ }
+ textarea {
+ height: auto;
+ }
+
+ /* grey out placeholders */
+ :-moz-placeholder {
+ color: #bfbfbf;
+ }
+ ::-webkit-input-placeholder {
+ color: #bfbfbf;
+ }
+
+ input[type=text],
+ input[type=password],
+ select,
+ textarea {
+ -webkit-transition: border linear 0.2s, box-shadow linear 0.2s;
+ -moz-transition: border linear 0.2s, box-shadow linear 0.2s;
+ transition: border linear 0.2s, box-shadow linear 0.2s;
+ -webkit-box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1);
+ -moz-box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1);
+ box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1);
+ }
+ input[type=text]:focus, input[type=password]:focus, textarea:focus {
+ outline: none;
+ border-color: rgba(82, 168, 236, 0.8);
+ -webkit-box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1), 0 0 8px rgba(82, 168, 236, 0.6);
+ -moz-box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1), 0 0 8px rgba(82, 168, 236, 0.6);
+ box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1), 0 0 8px rgba(82, 168, 236, 0.6);
+ }
+
+ /* buttons */
+ button {
+ display: inline-block;
+ padding: 4px 14px;
+ font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+ font-size: 13px;
+ line-height: 18px;
+ -webkit-border-radius: 4px;
+ -moz-border-radius: 4px;
+ border-radius: 4px;
+ -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);
+ -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);
+ box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);
+ background-color: #0064cd;
+ background-repeat: repeat-x;
+ background-image: -khtml-gradient(linear, left top, left bottom, from(#049cdb), to(#0064cd));
+ background-image: -moz-linear-gradient(top, #049cdb, #0064cd);
+ background-image: -ms-linear-gradient(top, #049cdb, #0064cd);
+ background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #049cdb), color-stop(100%, #0064cd));
+ background-image: -webkit-linear-gradient(top, #049cdb, #0064cd);
+ background-image: -o-linear-gradient(top, #049cdb, #0064cd);
+ background-image: linear-gradient(top, #049cdb, #0064cd);
+ color: #fff;
+ text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
+ border: 1px solid #004b9a;
+ border-bottom-color: #003f81;
+ -webkit-transition: 0.1s linear all;
+ -moz-transition: 0.1s linear all;
+ transition: 0.1s linear all;
+ border-color: #0064cd #0064cd #003f81;
+ border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
+ }
+ button:hover {
+ color: #fff;
+ background-position: 0 -15px;
+ text-decoration: none;
+ }
+ button:active {
+ -webkit-box-shadow: inset 0 3px 7px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05);
+ -moz-box-shadow: inset 0 3px 7px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05);
+ box-shadow: inset 0 3px 7px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05);
+ }
+ button::-moz-focus-inner {
+ padding: 0;
+ border: 0;
+ }
+ </style>
+</head>
+<body>
+ <div id="placeholder"></div>
+ <script>
+ 'use strict';
+
+ var placeholder = document.getElementById('placeholder');
+
+ var updateText = function(text) {
+ placeholder.innerHTML = marked(text);
+ }
+
+ new QWebChannel(qt.webChannelTransport,
+ function(channel) {
+ var content = channel.objects.content;
+ updateText(content.text);
+ content.textChanged.connect(updateText);
+ }
+ );
+ </script>
+</body>
+</html> \ No newline at end of file
diff --git a/src/sanitychecks.cpp b/src/sanitychecks.cpp
new file mode 100644
index 00000000..6da42e79
--- /dev/null
+++ b/src/sanitychecks.cpp
@@ -0,0 +1,357 @@
+#include "env.h"
+#include "envmodule.h"
+#include "settings.h"
+#include <iplugingame.h>
+#include <log.h>
+#include <utility.h>
+
+using namespace MOBase;
+
+enum class SecurityZone
+{
+ NoZone = -1,
+ MyComputer = 0,
+ Intranet = 1,
+ Trusted = 2,
+ Internet = 3,
+ Untrusted = 4,
+};
+
+QString toCodeName(SecurityZone z)
+{
+ switch (z)
+ {
+ case SecurityZone::NoZone: return "NoZone";
+ case SecurityZone::MyComputer: return "MyComputer";
+ case SecurityZone::Intranet: return "Intranet";
+ case SecurityZone::Trusted: return "Trusted";
+ case SecurityZone::Internet: return "Internet";
+ case SecurityZone::Untrusted: return "Untrusted";
+ default: return "Unknown zone";
+ }
+}
+
+QString toString(SecurityZone z)
+{
+ return QString("%1 (%2)")
+ .arg(toCodeName(z))
+ .arg(static_cast<int>(z));
+}
+
+// whether the given zone is considered blocked
+//
+bool isZoneBlocked(SecurityZone z)
+{
+ switch (z)
+ {
+ case SecurityZone::Internet:
+ case SecurityZone::Untrusted:
+ return true;
+
+ case SecurityZone::NoZone:
+ case SecurityZone::MyComputer:
+ case SecurityZone::Intranet:
+ case SecurityZone::Trusted:
+ default:
+ return false;
+ }
+}
+
+// whether the given file is blocked
+//
+bool isFileBlocked(const QFileInfo& fi)
+{
+ // name of the alternate data stream containing the zone identifier ini
+ const QString ads = "Zone.Identifier";
+
+ // key in the ini
+ const auto key = "ZoneTransfer/ZoneId";
+
+ // the path to the ADS is always `filename:Zone.Identifier`
+ const auto path = fi.absoluteFilePath();
+ const auto adsPath = path + ":" + ads;
+
+ QFile f(adsPath);
+ if (!f.exists()) {
+ // no ADS for this file
+ return false;
+ }
+
+ log::debug("'{}' has an ADS for {}", path, adsPath);
+
+ const QSettings qs(adsPath, QSettings::IniFormat);
+
+ // looking for key
+ if (!qs.contains(key)) {
+ log::debug("'{}': key '{}' not found", adsPath, key);
+ return false;
+ }
+
+ // getting value
+ const auto v = qs.value(key);
+ if (v.isNull()) {
+ log::debug("'{}': key '{}' is null", adsPath, key);
+ return false;
+ }
+
+ // should be an int
+ bool ok = false;
+ const auto z = static_cast<SecurityZone>(v.toInt(&ok));
+
+ if (!ok) {
+ log::debug("'{}': key '{}' is not an int (value is '{}')", adsPath, key, v);
+ return false;
+ }
+
+ if (!isZoneBlocked(z)) {
+ // that zone is not a blocked zone
+ log::debug("'{}': zone id is {}, which is fine", adsPath, toString(z));
+ return false;
+ }
+
+ // file is blocked
+ log::warn("{}", QObject::tr(
+ "'%1': file is blocked (%2)")
+ .arg(path)
+ .arg(toString(z)));
+
+ return true;
+}
+
+int checkBlockedFiles(const QDir& dir)
+{
+ // executables file types
+ const QStringList FileTypes = {"*.dll", "*.exe"};
+
+ if (!dir.exists()) {
+ // shouldn't happen
+ log::error(
+ "while checking for blocked files, directory '{}' not found",
+ dir.absolutePath());
+
+ return 1;
+ }
+
+ const auto files = dir.entryInfoList(FileTypes, QDir::Files);
+ if (files.empty()) {
+ // shouldn't happen
+ log::error(
+ "while checking for blocked files, directory '{}' is empty",
+ dir.absolutePath());
+
+ return 1;
+ }
+
+ int n = 0;
+
+ // checking each file in this directory
+ for (auto&& fi : files) {
+ if (isFileBlocked(fi)) {
+ ++n;
+ }
+ }
+
+ return n;
+}
+
+int checkBlocked()
+{
+ // directories that contain executables; these need to be explicit because
+ // portable instances might add billions of files in MO's directory
+ const QString dirs[] = {
+ ".",
+ "/dlls",
+ "/loot",
+ "/NCC",
+ "/platforms",
+ "/plugins"
+ };
+
+ log::debug(" . blocked files");
+ const QString appDir = QCoreApplication::applicationDirPath();
+
+ int n = 0;
+
+ for (const auto& d : dirs) {
+ const auto path = QDir(appDir + "/" + d).canonicalPath();
+ n += checkBlockedFiles(path);
+ }
+
+ return n;
+}
+
+int checkMissingFiles()
+{
+ // files that are likely to be eaten
+ static const QStringList files({
+ "helper.exe",
+ "nxmhandler.exe",
+ "usvfs_proxy_x64.exe",
+ "usvfs_proxy_x86.exe",
+ "usvfs_x64.dll",
+ "usvfs_x86.dll",
+ "loot/loot.dll",
+ "loot/lootcli.exe"
+ });
+
+ log::debug(" . missing files");
+ const auto dir = QCoreApplication::applicationDirPath();
+
+ int n = 0;
+
+ for (const auto& name : files) {
+ const QFileInfo file(dir + "/" + name);
+
+ if (!file.exists()) {
+ log::warn("{}", QObject::tr(
+ "'%1' seems to be missing, an antivirus may have deleted it")
+ .arg(file.absoluteFilePath()));
+
+ ++n;
+ }
+ }
+
+ return n;
+}
+
+int checkIncompatibleModule(const env::Module& m)
+{
+ // these dlls seems to interfere mostly with dialogs, like the mod info
+ // dialog: it renders dialogs fully white and makes it impossible to interact
+ // with them
+ //
+ // the dlls is usually loaded on startup, but there has been some reports
+ // where it got loaded later, so this is also called every time a new module
+ // is loaded into this process
+
+ static const std::map<QString, QString> names = {
+ {"NahimicOSD.dll", "Nahimic"},
+ {"RTSSHooks64.dll", "RivaTuner Statistics Server"},
+ {"SSAudioOSD.dll", "SteelSeries Audio"},
+ {"SS3DevProps.dll", "Sonic Suite 3"}
+ };
+
+ const QFileInfo file(m.path());
+ int n = 0;
+
+ for (auto&& p : names) {
+ if (file.fileName().compare(p.first, Qt::CaseInsensitive) == 0) {
+ log::warn("{}", QObject::tr(
+ "%1 is loaded. This program is known to cause issues with "
+ "Mod Organizer, such as freezing or blank windows. Consider "
+ "uninstalling it.")
+ .arg(p.second));
+
+ log::warn("{}", file.absoluteFilePath());
+
+ ++n;
+ }
+ }
+
+ return n;
+}
+
+int checkIncompatibilities(const env::Environment& e)
+{
+ log::debug(" . incompatibilities");
+
+ int n = 0;
+
+ for (auto&& m : e.loadedModules()) {
+ n += checkIncompatibleModule(m);
+ }
+
+ return n;
+}
+
+std::vector<std::pair<QString, QString>> getSystemDirectories()
+{
+ // folder ids and display names for logging
+ const std::vector<std::pair<GUID, QString>> systemFolderIDs = {
+ {FOLDERID_ProgramFiles, "Program Files"},
+ {FOLDERID_ProgramFilesX86, "Program Files"}
+ };
+
+ std::vector<std::pair<QString, QString>> systemDirs;
+
+ for (auto&& p : systemFolderIDs) {
+ try
+ {
+ const auto dir = MOBase::getKnownFolder(p.first);
+
+ auto path = QDir::toNativeSeparators(dir.absolutePath()).toLower();
+ if (!path.endsWith("\\")) {
+ path += "\\";
+ }
+
+ systemDirs.push_back({path, p.second});
+ }
+ catch(std::exception&)
+ {
+ // ignore
+ }
+ }
+
+ return systemDirs;
+}
+
+int checkProtected(const QDir& d, const QString& what)
+{
+ static const auto systemDirs = getSystemDirectories();
+
+ const auto path = QDir::toNativeSeparators(d.absolutePath()).toLower();
+
+ log::debug(" . {}: {}", what, path);
+
+ for (auto&& sd : systemDirs) {
+ if (path.startsWith(sd.first)) {
+ log::warn(
+ "{} is in {}; this may cause issues because it's a protected "
+ "system folder",
+ what, sd.second);
+
+ log::debug("path '{}' starts with '{}'", path, sd.first);
+
+ return 1;
+ }
+ }
+
+ return 0;
+}
+
+int checkPathsForSanity(IPluginGame& game, const Settings& s)
+{
+ log::debug("checking paths");
+
+ int n = 0;
+
+ n += checkProtected(game.gameDirectory(), "the game");
+ n += checkProtected(QApplication::applicationDirPath(), "Mod Organizer");
+
+ if (checkProtected(s.paths().base(), "the instance base directory")) {
+ ++n;
+ } else {
+ n += checkProtected(s.paths().downloads(), "the downloads directory");
+ n += checkProtected(s.paths().mods(), "the mods directory");
+ n += checkProtected(s.paths().cache(), "the cache directory");
+ n += checkProtected(s.paths().profiles(), "the profiles directory");
+ n += checkProtected(s.paths().overwrite(), "the overwrite directory");
+ }
+
+ return n;
+}
+
+void sanityChecks(const env::Environment& e)
+{
+ log::debug("running sanity checks...");
+
+ int n = 0;
+
+ n += checkBlocked();
+ n += checkMissingFiles();
+ n += checkIncompatibilities(e);
+
+ log::debug(
+ "sanity checks done, {}",
+ (n > 0 ? "problems were found" : "everything looks okay"));
+}
diff --git a/src/selfupdater.cpp b/src/selfupdater.cpp
index 271c621b..5a70568e 100644
--- a/src/selfupdater.cpp
+++ b/src/selfupdater.cpp
@@ -122,15 +122,25 @@ void SelfUpdater::setPluginContainer(PluginContainer *pluginContainer)
m_Interface->setPluginContainer(pluginContainer);
}
-void SelfUpdater::testForUpdate()
+void SelfUpdater::testForUpdate(const Settings& settings)
{
+ if (settings.network().offlineMode()) {
+ log::debug("not checking for updates, in offline mode");
+ return;
+ }
+
+ if (!settings.checkForUpdates()) {
+ log::debug("not checking for updates, disabled");
+ return;
+ }
+
// TODO: if prereleases are disabled we could just request the latest release
// directly
try {
m_GitHub.releases(GitHub::Repository("Modorganizer2", "modorganizer"),
[this](const QJsonArray &releases) {
if (releases.isEmpty()) {
- qDebug("Unable to connect to github.com to check version");
+ // error message already logged
return;
}
@@ -150,23 +160,23 @@ void SelfUpdater::testForUpdate()
VersionInfo newestVer(newest["tag_name"].toString());
if (newestVer > this->m_MOVersion) {
m_UpdateCandidate = newest;
- qDebug("update available: %s -> %s",
- qUtf8Printable(this->m_MOVersion.displayString(3)),
- qUtf8Printable(newestVer.displayString(3)));
+ log::debug("update available: {} -> {}",
+ this->m_MOVersion.displayString(3),
+ newestVer.displayString(3));
emit updateAvailable();
} else if (newestVer < this->m_MOVersion) {
// this could happen if the user switches from using prereleases to
// stable builds. Should we downgrade?
- qDebug("This version is newer than the latest released one: %s -> %s",
- qUtf8Printable(this->m_MOVersion.displayString(3)),
- qUtf8Printable(newestVer.displayString(3)));
+ log::debug("This version is newer than the latest released one: {} -> {}",
+ this->m_MOVersion.displayString(3),
+ newestVer.displayString(3));
}
}
});
}
//Catch all is bad by design, should be improved
catch (...) {
- qDebug("Unable to connect to github.com to check version");
+ log::debug("Unable to connect to github.com to check version");
}
}
@@ -230,7 +240,7 @@ void SelfUpdater::closeProgress()
void SelfUpdater::openOutputFile(const QString &fileName)
{
QString outputPath = QDir::fromNativeSeparators(qApp->property("dataPath").toString()) + "/" + fileName;
- qDebug("downloading to %s", qUtf8Printable(outputPath));
+ log::debug("downloading to {}", outputPath);
m_UpdateFile.setFileName(outputPath);
m_UpdateFile.open(QIODevice::WriteOnly);
}
@@ -312,7 +322,7 @@ void SelfUpdater::downloadFinished()
return;
}
- qDebug("download: %s", m_UpdateFile.fileName().toUtf8().constData());
+ log::debug("download: {}", m_UpdateFile.fileName());
try {
installUpdate();
@@ -331,11 +341,14 @@ void SelfUpdater::downloadCancel()
void SelfUpdater::installUpdate()
{
const QString parameters = "/DIR=\"" + qApp->applicationDirPath() + "\" ";
+ const auto r = shell::Execute(m_UpdateFile.fileName(), parameters);
- if (shell::Execute(m_UpdateFile.fileName(), parameters)) {
+ if (r.success()) {
QCoreApplication::quit();
} else {
- reportError(tr("Failed to start %1").arg(m_UpdateFile.fileName()));
+ reportError(tr("Failed to start %1: %2")
+ .arg(m_UpdateFile.fileName())
+ .arg(r.toString()));
}
m_UpdateFile.remove();
diff --git a/src/selfupdater.h b/src/selfupdater.h
index bce49495..0c81efc5 100644
--- a/src/selfupdater.h
+++ b/src/selfupdater.h
@@ -37,7 +37,7 @@ namespace MOBase { class IPluginGame; }
class QNetworkReply;
class QProgressDialog;
-
+class Settings;
/**
* @brief manages updates for Mod Organizer itself
@@ -81,6 +81,11 @@ public:
void setPluginContainer(PluginContainer *pluginContainer);
/**
+ * @brief request information about the current version
+ **/
+ void testForUpdate(const Settings& settings);
+
+ /**
* @brief start the update process
* @note this should not be called if there is no update available
**/
@@ -91,13 +96,6 @@ public:
**/
MOBase::VersionInfo getVersion() const { return m_MOVersion; }
-public slots:
-
- /**
- * @brief request information about the current version
- **/
- void testForUpdate();
-
signals:
/**
diff --git a/src/serverinfo.cpp b/src/serverinfo.cpp
index e96b69d2..aece61da 100644
--- a/src/serverinfo.cpp
+++ b/src/serverinfo.cpp
@@ -1 +1,178 @@
#include "serverinfo.h"
+#include "log.h"
+
+using namespace MOBase;
+
+const std::size_t MaxDownloadCount = 5;
+
+
+ServerInfo::ServerInfo()
+ : ServerInfo({}, false, {}, 0, {})
+{
+}
+
+ServerInfo::ServerInfo(
+ QString name, bool premium, QDate last, int preferred,
+ SpeedList lastDownloads) :
+ m_name(std::move(name)), m_premium(premium), m_lastSeen(std::move(last)),
+ m_preferred(preferred), m_lastDownloads(std::move(lastDownloads))
+{
+ if (m_lastDownloads.size() > MaxDownloadCount) {
+ m_lastDownloads.resize(MaxDownloadCount);
+ }
+}
+
+const QString& ServerInfo::name() const
+{
+ return m_name;
+}
+
+bool ServerInfo::isPremium() const
+{
+ return m_premium;
+}
+
+void ServerInfo::setPremium(bool b)
+{
+ m_premium = b;
+}
+
+const QDate& ServerInfo::lastSeen() const
+{
+ return m_lastSeen;
+}
+
+void ServerInfo::updateLastSeen()
+{
+ m_lastSeen = QDate::currentDate();
+}
+
+int ServerInfo::preferred() const
+{
+ return m_preferred;
+}
+
+void ServerInfo::setPreferred(int i)
+{
+ m_preferred = i;
+}
+
+const ServerInfo::SpeedList& ServerInfo::lastDownloads() const
+{
+ return m_lastDownloads;
+}
+
+int ServerInfo::averageSpeed() const
+{
+ int count = 0;
+ int total = 0;
+
+ for (const auto& s : m_lastDownloads) {
+ if (s > 0) {
+ ++count;
+ total += s;
+ }
+ }
+
+ if (count > 0) {
+ return static_cast<double>(total) / count;
+ }
+
+ return 0;
+}
+
+void ServerInfo::addDownload(int bytesPerSecond)
+{
+ if (bytesPerSecond <= 0) {
+ log::error(
+ "trying to add download with {} B/s to server '{}'; ignoring",
+ bytesPerSecond, m_name);
+
+ return;
+ }
+
+ if (m_lastDownloads.size() == MaxDownloadCount) {
+ std::rotate(
+ m_lastDownloads.begin(),
+ m_lastDownloads.begin() + 1,
+ m_lastDownloads.end());
+
+ m_lastDownloads.back() = bytesPerSecond;
+ } else {
+ m_lastDownloads.push_back(bytesPerSecond);
+ }
+
+ log::debug("added download at {} B/s to server '{}'", bytesPerSecond, m_name);
+}
+
+
+void ServerList::add(ServerInfo s)
+{
+ m_servers.push_back(std::move(s));
+
+ std::sort(m_servers.begin(), m_servers.end(), [](auto&& a, auto&& b){
+ return (a.preferred() < b.preferred());
+ });
+}
+
+ServerList::iterator ServerList::begin()
+{
+ return m_servers.begin();
+}
+
+ServerList::const_iterator ServerList::begin() const
+{
+ return m_servers.begin();
+}
+
+ServerList::iterator ServerList::end()
+{
+ return m_servers.end();
+}
+
+ServerList::const_iterator ServerList::end() const
+{
+ return m_servers.end();
+}
+
+std::size_t ServerList::size() const
+{
+ return m_servers.size();
+}
+
+bool ServerList::empty() const
+{
+ return m_servers.empty();
+}
+
+ServerList::container ServerList::getPreferred() const
+{
+ container v;
+
+ for (const auto& server : m_servers) {
+ if (server.preferred() > 0) {
+ v.push_back(server);
+ }
+ }
+
+ return v;
+}
+
+void ServerList::cleanup()
+{
+ QDate now = QDate::currentDate();
+
+ for (auto itor=m_servers.begin(); itor!=m_servers.end(); ) {
+ const QDate lastSeen = itor->lastSeen();
+
+ if (lastSeen.daysTo(now) > 30) {
+ log::debug(
+ "removing server {} since it hasn't been available for downloads "
+ "in over a month", itor->name());
+
+ itor = m_servers.erase(itor);
+ } else {
+ ++itor;
+ }
+ }
+}
diff --git a/src/serverinfo.h b/src/serverinfo.h
index 8e5e935a..af8f77c8 100644
--- a/src/serverinfo.h
+++ b/src/serverinfo.h
@@ -5,14 +5,66 @@
#include <QDate>
#include <QMetaType>
-struct ServerInfo
+class ServerInfo
{
- QString name;
- bool premium;
- QDate lastSeen;
- bool preferred;
+public:
+ using SpeedList = std::vector<int>;
+
+ ServerInfo();
+ ServerInfo(
+ QString name, bool premium, QDate lastSeen, int preferred,
+ SpeedList lastDownloads);
+
+ const QString& name() const;
+
+ bool isPremium() const;
+ void setPremium(bool b);
+
+ const QDate& lastSeen() const;
+ void updateLastSeen();
+
+ int preferred() const;
+ void setPreferred(int i);
+
+ const SpeedList& lastDownloads() const;
+ int averageSpeed() const;
+ void addDownload(int bytesPerSecond);
+
+private:
+ QString m_name;
+ bool m_premium;
+ QDate m_lastSeen;
+ int m_preferred;
+ SpeedList m_lastDownloads;
};
Q_DECLARE_METATYPE(ServerInfo)
+
+class ServerList
+{
+public:
+ using container = std::vector<ServerInfo>;
+ using iterator = container::iterator;
+ using const_iterator = container::const_iterator;
+
+ void add(ServerInfo s);
+
+ iterator begin();
+ const_iterator begin() const;
+ iterator end();
+ const_iterator end() const;
+ std::size_t size() const;
+ bool empty() const;
+
+ container getPreferred() const;
+
+ // removes servers that haven't been seen in a while
+ //
+ void cleanup();
+
+private:
+ container m_servers;
+};
+
#endif // SERVERINFO_H
diff --git a/src/settings.cpp b/src/settings.cpp
index 5cb2524f..5170a5de 100644
--- a/src/settings.cpp
+++ b/src/settings.cpp
@@ -18,75 +18,55 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
*/
#include "settings.h"
-
-#include "pluginsetting.h"
+#include "settingsutilities.h"
#include "serverinfo.h"
-#include "settingsdialog.h"
-#include "versioninfo.h"
+#include "executableslist.h"
#include "appconfig.h"
-#include "organizercore.h"
+#include "env.h"
+#include "envmetrics.h"
+#include <expanderwidget.h>
#include <utility.h>
-#include <iplugin.h>
#include <iplugingame.h>
-#include <questionboxmemory.h>
-#include <usvfsparameters.h>
-
-#include <QCheckBox>
-#include <QCoreApplication>
-#include <QComboBox>
-#include <QDate>
-#include <QDialog>
-#include <QDir>
-#include <QDirIterator>
-#include <QFileInfo>
-#include <QLineEdit>
-#include <QSpinBox>
-#include <QListWidgetItem>
-#include <QLocale>
-#include <QMessageBox>
-#include <QApplication>
-#include <QRegExp>
-#include <QDir>
-#include <QStringList>
-#include <QVariantMap>
-#include <QLabel>
-#include <QPushButton>
-#include <QPalette>
-#include <Qt> // for Qt::UserRole, etc
-#include <QtDebug> // for qDebug, qWarning
-
-#include <Windows.h> // For ShellExecuteW, HINSTANCE, etc
-#include <wincred.h> // For storage
+using namespace MOBase;
-#include <algorithm> // for sort
-#include <memory>
-#include <stdexcept> // for runtime_error
-#include <string>
-#include <utility> // for pair, make_pair
+EndorsementState endorsementStateFromString(const QString& s)
+{
+ if (s == "Endorsed") {
+ return EndorsementState::Accepted;
+ } else if (s == "Abstained") {
+ return EndorsementState::Refused;
+ } else {
+ return EndorsementState::NoDecision;
+ }
+}
-using namespace MOBase;
+QString toString(EndorsementState s)
+{
+ switch (s)
+ {
+ case EndorsementState::Accepted:
+ return "Endorsed";
-template <typename T>
-class QListWidgetItemEx : public QListWidgetItem {
-public:
- QListWidgetItemEx(const QString &text, int sortRole = Qt::DisplayRole, QListWidget *parent = 0, int type = Type)
- : QListWidgetItem(text, parent, type), m_SortRole(sortRole) {}
+ case EndorsementState::Refused:
+ return "Abstained";
- virtual bool operator< ( const QListWidgetItem & other ) const {
- return this->data(m_SortRole).value<T>() < other.data(m_SortRole).value<T>();
+ case EndorsementState::NoDecision: // fall-through
+ default:
+ return {};
}
-private:
- int m_SortRole;
-};
+}
Settings *Settings::s_Instance = nullptr;
-
-Settings::Settings(const QSettings &settingsSource)
- : m_Settings(settingsSource.fileName(), settingsSource.format())
+Settings::Settings(const QString& path) :
+ m_Settings(path, QSettings::IniFormat),
+ m_Game(m_Settings), m_Geometry(m_Settings), m_Widgets(m_Settings),
+ m_Colors(m_Settings), m_Plugins(m_Settings), m_Paths(m_Settings),
+ m_Network(m_Settings), m_Nexus(*this, m_Settings), m_Steam(*this, m_Settings),
+ m_Interface(m_Settings), m_Diagnostics(m_Settings)
{
if (s_Instance != nullptr) {
throw std::runtime_error("second instance of \"Settings\" created");
@@ -95,13 +75,12 @@ Settings::Settings(const QSettings &settingsSource)
}
}
-
Settings::~Settings()
{
+ MOBase::QuestionBoxMemory::setCallbacks({}, {}, {});
s_Instance = nullptr;
}
-
Settings &Settings::instance()
{
if (s_Instance == nullptr) {
@@ -110,447 +89,1188 @@ Settings &Settings::instance()
return *s_Instance;
}
-void Settings::clearPlugins()
+void Settings::processUpdates(
+ const QVersionNumber& currentVersion, const QVersionNumber& lastVersion)
{
- m_Plugins.clear();
- m_PluginSettings.clear();
+ if (firstStart()) {
+ return;
+ }
- m_PluginBlacklist.clear();
- int count = m_Settings.beginReadArray("pluginBlacklist");
- for (int i = 0; i < count; ++i) {
- m_Settings.setArrayIndex(i);
- m_PluginBlacklist.insert(m_Settings.value("name").toString());
+ if (currentVersion == lastVersion) {
+ return;
}
- m_Settings.endArray();
+
+ log::info(
+ "updating from {} to {}",
+ lastVersion.toString(), currentVersion.toString());
+
+ auto version = [&](const QVersionNumber& v, auto&& f) {
+ if (lastVersion < v) {
+ log::debug("processing updates for {}", v.toString());
+ f();
+ }
+ };
+
+ version({2, 2, 0}, [&]{
+ remove(m_Settings, "Settings", "steam_password");
+ remove(m_Settings, "Settings", "nexus_username");
+ remove(m_Settings, "Settings", "nexus_password");
+ remove(m_Settings, "Settings", "nexus_login");
+ remove(m_Settings, "Settings", "nexus_api_key");
+ remove(m_Settings, "Settings", "ask_for_nexuspw");
+ remove(m_Settings, "Settings", "nmm_version");
+
+ removeSection(m_Settings, "Servers");
+ });
+
+ version({2, 2, 1}, [&]{
+ remove(m_Settings, "General", "mod_info_tabs");
+ remove(m_Settings, "General", "mod_info_conflict_expanders");
+ remove(m_Settings, "General", "mod_info_conflicts");
+ remove(m_Settings, "General", "mod_info_advanced_conflicts");
+ remove(m_Settings, "General", "mod_info_conflicts_overwrite");
+ remove(m_Settings, "General", "mod_info_conflicts_noconflict");
+ remove(m_Settings, "General", "mod_info_conflicts_overwritten");
+ });
+
+ version({2, 2, 2}, [&]{
+ // log splitter is gone, it's a dock now
+ remove(m_Settings, "General", "log_split");
+
+ // moved to widgets
+ remove(m_Settings, "General", "mod_info_conflicts_tab");
+ remove(m_Settings, "General", "mod_info_conflicts_general_expanders");
+ remove(m_Settings, "General", "mod_info_conflicts_general_overwrite");
+ remove(m_Settings, "General", "mod_info_conflicts_general_noconflict");
+ remove(m_Settings, "General", "mod_info_conflicts_general_overwritten");
+ remove(m_Settings, "General", "mod_info_conflicts_advanced_list");
+ remove(m_Settings, "General", "mod_info_conflicts_advanced_options");
+ remove(m_Settings, "General", "mod_info_tab_order");
+ remove(m_Settings, "General", "mod_info_dialog_images_show_dds");
+
+ // moved to geometry
+ remove(m_Settings, "General", "window_geometry");
+ remove(m_Settings, "General", "window_state");
+ remove(m_Settings, "General", "toolbar_size");
+ remove(m_Settings, "General", "toolbar_button_style");
+ remove(m_Settings, "General", "menubar_visible");
+ remove(m_Settings, "General", "statusbar_visible");
+ remove(m_Settings, "General", "window_split");
+ remove(m_Settings, "General", "window_monitor");
+ remove(m_Settings, "General", "browser_geometry");
+ remove(m_Settings, "General", "filters_visible");
+
+ // this was supposed to have been removed above when updating from 2.2.0,
+ // but it wasn't in Settings, it was in General
+ remove(m_Settings, "General", "ask_for_nexuspw");
+
+ m_Network.updateFromOldMap();
+ });
+
+ //save version in all case
+ set(m_Settings, "General", "version", currentVersion.toString());
+
+ log::debug("updating done");
}
-bool Settings::pluginBlacklisted(const QString &fileName) const
+QString Settings::filename() const
{
- return m_PluginBlacklist.contains(fileName);
+ return m_Settings.fileName();
}
-void Settings::registerAsNXMHandler(bool force)
+bool Settings::checkForUpdates() const
{
- const auto nxmPath = QCoreApplication::applicationDirPath() + "/nxmhandler.exe";
- const auto executable = QCoreApplication::applicationFilePath();
+ return get<bool>(m_Settings, "Settings", "check_for_updates", true);
+}
- QString mode = force ? "forcereg" : "reg";
- QString parameters = mode + " " + m_GamePlugin->gameShortName();
- for (const QString& altGame : m_GamePlugin->validShortNames()) {
- parameters += "," + altGame;
+void Settings::setCheckForUpdates(bool b)
+{
+ set(m_Settings, "Settings", "check_for_updates", b);
+}
+
+bool Settings::usePrereleases() const
+{
+ return get<bool>(m_Settings, "Settings", "use_prereleases", false);
+}
+
+void Settings::setUsePrereleases(bool b)
+{
+ set(m_Settings, "Settings", "use_prereleases", b);
+}
+
+std::optional<QVersionNumber> Settings::version() const
+{
+ if (auto v=getOptional<QString>(m_Settings, "General", "version")) {
+ return QVersionNumber::fromString(*v).normalized();
}
- parameters += " \"" + executable + "\"";
- if (!shell::Execute(nxmPath, parameters)) {
- QMessageBox::critical(
- nullptr, tr("Failed"), tr("Failed to start the helper application"));
+ return {};
+}
+
+bool Settings::firstStart() const
+{
+ return get<bool>(m_Settings, "General", "first_start", true);
+}
+
+void Settings::setFirstStart(bool b)
+{
+ set(m_Settings, "General", "first_start", b);
+}
+
+QString Settings::executablesBlacklist() const
+{
+ static const QString def = (QStringList()
+ << "Chrome.exe"
+ << "Firefox.exe"
+ << "TSVNCache.exe"
+ << "TGitCache.exe"
+ << "Steam.exe"
+ << "GameOverlayUI.exe"
+ << "Discord.exe"
+ << "GalaxyClient.exe"
+ << "Spotify.exe"
+ ).join(";");
+
+ return get<QString>(m_Settings, "Settings", "executable_blacklist", def);
+}
+
+bool Settings::isExecutableBlacklisted(const QString& s) const
+{
+ for (auto exec : executablesBlacklist().split(";")) {
+ if (exec.compare(s, Qt::CaseInsensitive) == 0) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+void Settings::setExecutablesBlacklist(const QString& s)
+{
+ set(m_Settings, "Settings", "executable_blacklist", s);
+}
+
+void Settings::setMotdHash(uint hash)
+{
+ set(m_Settings, "General", "motd_hash", hash);
+}
+
+unsigned int Settings::motdHash() const
+{
+ return get<unsigned int>(m_Settings, "General", "motd_hash", 0);
+}
+
+bool Settings::archiveParsing() const
+{
+ return get<bool>(m_Settings, "Settings", "archive_parsing_experimental", false);
+}
+
+void Settings::setArchiveParsing(bool b)
+{
+ set(m_Settings, "Settings", "archive_parsing_experimental", b);
+}
+
+std::vector<std::map<QString, QVariant>> Settings::executables() const
+{
+ ScopedReadArray sra(m_Settings, "customExecutables");
+ std::vector<std::map<QString, QVariant>> v;
+
+ sra.for_each([&]{
+ std::map<QString, QVariant> map;
+
+ for (auto&& key : sra.keys()) {
+ map[key] = sra.get<QVariant>(key);
+ }
+
+ v.push_back(map);
+ });
+
+ return v;
+}
+
+void Settings::setExecutables(const std::vector<std::map<QString, QVariant>>& v)
+{
+ const auto current = executables();
+
+ if (current == v) {
+ // no change
+ return;
}
+
+ if (current.size() > v.size()) {
+ // Qt can't remove array elements, the section must be cleared
+ removeSection(m_Settings, "customExecutables");
+ }
+
+ ScopedWriteArray swa(m_Settings, "customExecutables", v.size());
+
+ for (const auto& map : v) {
+ swa.next();
+
+ for (auto&& p : map) {
+ swa.set(p.first, p.second);
+ }
+ }
+}
+
+bool Settings::keepBackupOnInstall() const
+{
+ return get<bool>(m_Settings, "General", "backup_install", false);
+}
+
+void Settings::setKeepBackupOnInstall(bool b)
+{
+ set(m_Settings, "General", "backup_install", b);
+}
+
+GameSettings& Settings::game()
+{
+ return m_Game;
+}
+
+const GameSettings& Settings::game() const
+{
+ return m_Game;
+}
+
+GeometrySettings& Settings::geometry()
+{
+ return m_Geometry;
+}
+
+const GeometrySettings& Settings::geometry() const
+{
+ return m_Geometry;
+}
+
+WidgetSettings& Settings::widgets()
+{
+ return m_Widgets;
+}
+
+const WidgetSettings& Settings::widgets() const
+{
+ return m_Widgets;
+}
+
+ColorSettings& Settings::colors()
+{
+ return m_Colors;
+}
+
+const ColorSettings& Settings::colors() const
+{
+ return m_Colors;
+}
+
+PluginSettings& Settings::plugins()
+{
+ return m_Plugins;
+}
+
+const PluginSettings& Settings::plugins() const
+{
+ return m_Plugins;
+}
+
+PathSettings& Settings::paths()
+{
+ return m_Paths;
+}
+
+const PathSettings& Settings::paths() const
+{
+ return m_Paths;
}
-bool Settings::colorSeparatorScrollbar() const
+NetworkSettings& Settings::network()
{
- return m_Settings.value("Settings/colorSeparatorScrollbars", true).toBool();
+ return m_Network;
+}
+
+const NetworkSettings& Settings::network() const
+{
+ return m_Network;
+}
+
+NexusSettings& Settings::nexus()
+{
+ return m_Nexus;
+}
+
+const NexusSettings& Settings::nexus() const
+{
+ return m_Nexus;
+}
+
+SteamSettings& Settings::steam()
+{
+ return m_Steam;
+}
+
+const SteamSettings& Settings::steam() const
+{
+ return m_Steam;
+}
+
+InterfaceSettings& Settings::interface()
+{
+ return m_Interface;
+}
+
+const InterfaceSettings& Settings::interface() const
+{
+ return m_Interface;
+}
+
+DiagnosticsSettings& Settings::diagnostics()
+{
+ return m_Diagnostics;
+}
+
+const DiagnosticsSettings& Settings::diagnostics() const
+{
+ return m_Diagnostics;
+}
+
+QSettings::Status Settings::sync() const
+{
+ m_Settings.sync();
+ return m_Settings.status();
+}
+
+void Settings::dump() const
+{
+ static const QStringList ignore({
+ "username", "password", "nexus_api_key"
+ });
+
+ log::debug("settings:");
+
+ {
+ ScopedGroup sg(m_Settings, "Settings");
+
+ for (auto k : m_Settings.allKeys()) {
+ if (ignore.contains(k, Qt::CaseInsensitive)) {
+ continue;
+ }
+
+ log::debug(" . {}={}", k, m_Settings.value(k).toString());
+ }
+ }
+
+ m_Network.dump();
}
void Settings::managedGameChanged(IPluginGame const *gamePlugin)
{
+ m_Game.setPlugin(gamePlugin);
+}
+
+
+GameSettings::GameSettings(QSettings& settings)
+ : m_Settings(settings), m_GamePlugin(nullptr)
+{
+}
+
+const MOBase::IPluginGame* GameSettings::plugin()
+{
+ return m_GamePlugin;
+}
+
+void GameSettings::setPlugin(const MOBase::IPluginGame* gamePlugin)
+{
m_GamePlugin = gamePlugin;
}
-void Settings::registerPlugin(IPlugin *plugin)
+bool GameSettings::forceEnableCoreFiles() const
{
- m_Plugins.push_back(plugin);
- m_PluginSettings.insert(plugin->name(), QVariantMap());
- m_PluginDescriptions.insert(plugin->name(), QVariantMap());
- for (const PluginSetting &setting : plugin->settings()) {
- QVariant temp = m_Settings.value("Plugins/" + plugin->name() + "/" + setting.key, setting.defaultValue);
- if (!temp.convert(setting.defaultValue.type())) {
- qWarning("failed to interpret \"%s\" as correct type for \"%s\" in plugin \"%s\", using default",
- qUtf8Printable(temp.toString()), qUtf8Printable(setting.key), qUtf8Printable(plugin->name()));
- temp = setting.defaultValue;
- }
- m_PluginSettings[plugin->name()][setting.key] = temp;
- m_PluginDescriptions[plugin->name()][setting.key] = QString("%1 (default: %2)").arg(setting.description).arg(setting.defaultValue.toString());
+ return get<bool>(m_Settings, "Settings", "force_enable_core_files", true);
+}
+
+void GameSettings::setForceEnableCoreFiles(bool b)
+{
+ set(m_Settings, "Settings", "force_enable_core_files", b);
+}
+
+std::optional<QString> GameSettings::directory() const
+{
+ if (auto v=getOptional<QByteArray>(m_Settings, "General", "gamePath")) {
+ return QString::fromUtf8(*v);
}
+
+ return {};
}
-bool Settings::obfuscate(const QString key, const QString data)
+void GameSettings::setDirectory(const QString& path)
{
- QString finalKey("ModOrganizer2_" + key);
- wchar_t* keyData = new wchar_t[finalKey.size()+1];
- finalKey.toWCharArray(keyData);
- keyData[finalKey.size()] = L'\0';
- bool result = false;
- if (data.isEmpty()) {
- result = CredDeleteW(keyData, CRED_TYPE_GENERIC, 0);
- if (!result)
- if (GetLastError() == ERROR_NOT_FOUND)
- result = true;
- } else {
- wchar_t* charData = new wchar_t[data.size()];
- data.toWCharArray(charData);
+ set(m_Settings, "General", "gamePath", QDir::toNativeSeparators(path).toUtf8());
+}
- CREDENTIALW cred = {};
- cred.Flags = 0;
- cred.Type = CRED_TYPE_GENERIC;
- cred.TargetName = keyData;
- cred.CredentialBlob = (LPBYTE)charData;
- cred.CredentialBlobSize = sizeof(wchar_t) * data.size();
- cred.Persist = CRED_PERSIST_LOCAL_MACHINE;
+std::optional<QString> GameSettings::name() const
+{
+ return getOptional<QString>(m_Settings, "General", "gameName");
+}
- result = CredWriteW(&cred, 0);
- delete[] charData;
+void GameSettings::setName(const QString& name)
+{
+ set(m_Settings, "General", "gameName", name);
+}
+
+std::optional<QString> GameSettings::edition() const
+{
+ return getOptional<QString>(m_Settings, "General", "game_edition");
+}
+
+void GameSettings::setEdition(const QString& name)
+{
+ set(m_Settings, "General", "game_edition", name);
+}
+
+std::optional<QString> GameSettings::selectedProfileName() const
+{
+ if (auto v=getOptional<QByteArray>(m_Settings, "General", "selected_profile")) {
+ return QString::fromUtf8(*v);
}
- delete[] keyData;
- return result;
+
+ return {};
}
-QString Settings::deObfuscate(const QString key)
+void GameSettings::setSelectedProfileName(const QString& name)
{
- QString result;
- QString finalKey("ModOrganizer2_" + key);
- wchar_t* keyData = new wchar_t[finalKey.size()+1];
- finalKey.toWCharArray(keyData);
- keyData[finalKey.size()] = L'\0';
- PCREDENTIALW creds;
- if (CredReadW(keyData, 1, 0, &creds)) {
- wchar_t *charData = (wchar_t *)creds->CredentialBlob;
- result = QString::fromWCharArray(charData, creds->CredentialBlobSize / sizeof(wchar_t));
- CredFree(creds);
- } else {
- const auto e = GetLastError();
- if (e != ERROR_NOT_FOUND) {
- qCritical().nospace()
- << "Retrieving encrypted data failed: "
- << formatSystemMessageQ(e);
+ set(m_Settings, "General", "selected_profile", name.toUtf8());
+}
+
+LoadMechanism::EMechanism GameSettings::loadMechanismType() const
+{
+ const auto def = LoadMechanism::LOAD_MODORGANIZER;
+
+ const auto i = get<LoadMechanism::EMechanism>(m_Settings,
+ "Settings", "load_mechanism", def);
+
+ switch (i)
+ {
+ // ok
+ case LoadMechanism::LOAD_MODORGANIZER: // fall-through
+ {
+ break;
+ }
+
+ default:
+ {
+ log::error(
+ "invalid load mechanism {}, reverting to {}",
+ static_cast<int>(i), toString(def));
+
+ set(m_Settings, "Settings", "load_mechanism", def);
+
+ return def;
}
}
- delete[] keyData;
- return result;
+
+ return i;
}
-QColor Settings::getIdealTextColor(const QColor& rBackgroundColor)
+void GameSettings::setLoadMechanism(LoadMechanism::EMechanism m)
{
- if (rBackgroundColor.alpha() == 0)
- return QColor(Qt::black);
+ set(m_Settings, "Settings", "load_mechanism", m);
+}
- const int THRESHOLD = 106 * 255.0f / rBackgroundColor.alpha();
- int BackgroundDelta = (rBackgroundColor.red() * 0.299) + (rBackgroundColor.green() * 0.587) + (rBackgroundColor.blue() * 0.114);
- return QColor((255 - BackgroundDelta <= THRESHOLD) ? Qt::black : Qt::white);
+const LoadMechanism& GameSettings::loadMechanism() const
+{
+ return m_LoadMechanism;
}
+LoadMechanism& GameSettings::loadMechanism()
+{
+ return m_LoadMechanism;
+}
-bool Settings::hideUncheckedPlugins() const
+bool GameSettings::hideUncheckedPlugins() const
{
- return m_Settings.value("Settings/hide_unchecked_plugins", false).toBool();
+ return get<bool>(m_Settings, "Settings", "hide_unchecked_plugins", false);
}
-bool Settings::forceEnableCoreFiles() const
+void GameSettings::setHideUncheckedPlugins(bool b)
{
- return m_Settings.value("Settings/force_enable_core_files", true).toBool();
+ set(m_Settings, "Settings", "hide_unchecked_plugins", b);
}
-bool Settings::lockGUI() const
+
+GeometrySettings::GeometrySettings(QSettings& s)
+ : m_Settings(s), m_Reset(false)
{
- return m_Settings.value("Settings/lock_gui", true).toBool();
}
-bool Settings::automaticLoginEnabled() const
+void GeometrySettings::requestReset()
{
- return m_Settings.value("Settings/nexus_login", false).toBool();
+ m_Reset = true;
}
-QString Settings::getSteamAppID() const
+void GeometrySettings::resetIfNeeded()
{
- return m_Settings.value("Settings/app_id", m_GamePlugin->steamAPPId()).toString();
+ if (!m_Reset) {
+ return;
+ }
+
+ removeSection(m_Settings, "Geometry");
}
-bool Settings::usePrereleases() const
+void GeometrySettings::saveGeometry(const QMainWindow* w)
{
- return m_Settings.value("Settings/use_prereleases", false).toBool();
+ saveWindowGeometry(w);
}
-void Settings::setDownloadSpeed(const QString &serverName, int bytesPerSecond)
+bool GeometrySettings::restoreGeometry(QMainWindow* w) const
{
- m_Settings.beginGroup("Servers");
+ return restoreWindowGeometry(w);
+}
- for (const QString &serverKey : m_Settings.childKeys()) {
- QVariantMap data = m_Settings.value(serverKey).toMap();
- if (serverKey == serverName) {
- data["downloadCount"] = data["downloadCount"].toInt() + 1;
- data["downloadSpeed"] = data["downloadSpeed"].toDouble() + static_cast<double>(bytesPerSecond);
- m_Settings.setValue(serverKey, data);
- }
+void GeometrySettings::saveGeometry(const QDialog* d)
+{
+ saveWindowGeometry(d);
+}
+
+bool GeometrySettings::restoreGeometry(QDialog* d) const
+{
+ const auto r = restoreWindowGeometry(d);
+
+ if (centerDialogs()) {
+ centerOnParent(d);
}
- m_Settings.endGroup();
- m_Settings.sync();
+ return r;
+}
+
+void GeometrySettings::saveWindowGeometry(const QWidget* w)
+{
+ set(m_Settings, "Geometry", geoSettingName(w), w->saveGeometry());
}
-std::map<QString, int> Settings::getPreferredServers()
+bool GeometrySettings::restoreWindowGeometry(QWidget* w) const
{
- std::map<QString, int> result;
- m_Settings.beginGroup("Servers");
+ if (auto v=getOptional<QByteArray>(m_Settings, "Geometry", geoSettingName(w))) {
+ w->restoreGeometry(*v);
+ ensureWindowOnScreen(w);
+ return true;
+ }
- for (const QString &serverKey : m_Settings.childKeys()) {
- QVariantMap data = m_Settings.value(serverKey).toMap();
- int preference = data["preferred"].toInt();
- if (preference > 0) {
- result[serverKey] = preference;
+ return false;
+}
+
+void GeometrySettings::ensureWindowOnScreen(QWidget* w) const
+{
+ // users report that the main window and/or dialogs are displayed off-screen;
+ // the usual workaround is keyboard navigation to move it
+ //
+ // qt should have code that deals with multiple monitors and off-screen
+ // geometries, but there seems to be bugs or inconsistencies that can't be
+ // reproduced
+ //
+ // the closest would probably be https://bugreports.qt.io/browse/QTBUG-64498,
+ // which is about multiple monitors and high dpi, but it seems fixed as of
+ // 5.12.4, which is shipped with 2.2.1
+ //
+ // without being to reproduce the problem, some simple checks are made in a
+ // timer, which may mitigate the issues
+
+ QTimer::singleShot(100, w, [w] {
+ const auto borders = 20;
+
+ // desktop geometry, made smaller to make sure there isn't just a few pixels
+ const auto originalDg = env::Environment().metrics().desktopGeometry();
+ const auto dg = originalDg.adjusted(borders, borders, -borders, -borders);
+
+ const auto g = w->geometry();
+
+ if (!dg.intersects(g)) {
+ log::warn(
+ "window '{}' is offscreen, moving to main monitor; geo={}, desktop={}",
+ w->objectName(), g, originalDg);
+
+ // widget is off-screen, center it on main monitor
+ centerOnMonitor(w, -1);
+
+ log::warn("window '{}' now at {}", w->objectName(), w->geometry());
}
+ });
+}
+
+void GeometrySettings::saveState(const QMainWindow* w)
+{
+ set(m_Settings, "Geometry", stateSettingName(w), w->saveState());
+}
+
+bool GeometrySettings::restoreState(QMainWindow* w) const
+{
+ if (auto v=getOptional<QByteArray>(m_Settings, "Geometry", stateSettingName(w))) {
+ w->restoreState(*v);
+ return true;
}
- m_Settings.endGroup();
- return result;
+ return false;
}
-QString Settings::getConfigurablePath(const QString &key,
- const QString &def,
- bool resolve) const
+void GeometrySettings::saveState(const QHeaderView* w)
{
- QString result = QDir::fromNativeSeparators(
- m_Settings.value(QString("settings/") + key, QString("%BASE_DIR%/") + def)
- .toString());
- if (resolve) {
- result.replace("%BASE_DIR%", getBaseDirectory());
+ set(m_Settings, "Geometry", stateSettingName(w), w->saveState());
+}
+
+bool GeometrySettings::restoreState(QHeaderView* w) const
+{
+ if (auto v=getOptional<QByteArray>(m_Settings, "Geometry", stateSettingName(w))) {
+ w->restoreState(*v);
+ return true;
}
- return result;
+
+ return false;
}
-QString Settings::getBaseDirectory() const
+void GeometrySettings::saveState(const QSplitter* w)
{
- return QDir::fromNativeSeparators(m_Settings.value(
- "settings/base_directory", qApp->property("dataPath").toString()).toString());
+ set(m_Settings, "Geometry", stateSettingName(w), w->saveState());
}
-QString Settings::getDownloadDirectory(bool resolve) const
+bool GeometrySettings::restoreState(QSplitter* w) const
{
- return getConfigurablePath("download_directory", ToQString(AppConfig::downloadPath()), resolve);
+ if (auto v=getOptional<QByteArray>(m_Settings, "Geometry", stateSettingName(w))) {
+ w->restoreState(*v);
+ return true;
+ }
+
+ return false;
}
-QString Settings::getCacheDirectory(bool resolve) const
+void GeometrySettings::saveState(const ExpanderWidget* expander)
{
- return getConfigurablePath("cache_directory", ToQString(AppConfig::cachePath()), resolve);
+ set(m_Settings, "Geometry", stateSettingName(expander), expander->saveState());
}
-QString Settings::getModDirectory(bool resolve) const
+bool GeometrySettings::restoreState(ExpanderWidget* expander) const
{
- return getConfigurablePath("mod_directory", ToQString(AppConfig::modsPath()), resolve);
+ if (auto v=getOptional<QByteArray>(m_Settings, "Geometry", stateSettingName(expander))) {
+ expander->restoreState(*v);
+ return true;
+ }
+
+ return false;
}
-QString Settings::getManagedGameDirectory() const
+void GeometrySettings::saveVisibility(const QWidget* w)
{
- return m_Settings.value("gamePath", "").toString();
+ set(m_Settings, "Geometry", visibilitySettingName(w), w->isVisible());
}
-QString Settings::getProfileDirectory(bool resolve) const
+bool GeometrySettings::restoreVisibility(QWidget* w, std::optional<bool> def) const
{
- return getConfigurablePath("profiles_directory", ToQString(AppConfig::profilesPath()), resolve);
+ if (auto v=getOptional<bool>(m_Settings, "Geometry", visibilitySettingName(w), def)) {
+ w->setVisible(*v);
+ return true;
+ }
+
+ return false;
}
-QString Settings::getOverwriteDirectory(bool resolve) const
+void GeometrySettings::restoreToolbars(QMainWindow* w) const
{
- return getConfigurablePath("overwrite_directory",
- ToQString(AppConfig::overwritePath()), resolve);
+ // all toolbars have the same size and button style settings
+ const auto size = getOptional<QSize>(m_Settings, "Geometry", "toolbar_size");
+ const auto style = getOptional<int>(m_Settings, "Geometry", "toolbar_button_style");
+
+ for (auto* tb : w->findChildren<QToolBar*>()) {
+ if (size) {
+ tb->setIconSize(*size);
+ }
+
+ if (style) {
+ tb->setToolButtonStyle(static_cast<Qt::ToolButtonStyle>(*style));
+ }
+
+ restoreVisibility(tb);
+ }
}
-bool Settings::getNexusApiKey(QString &apiKey) const
+void GeometrySettings::saveToolbars(const QMainWindow* w)
{
- QString tempKey = deObfuscate("APIKEY");
- if (tempKey.isEmpty())
- return false;
- apiKey = tempKey;
- return true;
+ const auto tbs = w->findChildren<QToolBar*>();
+
+ // save visibility for all
+ for (auto* tb : tbs) {
+ saveVisibility(tb);
+ }
+
+ // all toolbars have the same size and button style settings, just save the
+ // first one
+ if (!tbs.isEmpty()) {
+ const auto* tb = tbs[0];
+
+ set(m_Settings, "Geometry", "toolbar_size", tb->iconSize());
+ set(m_Settings, "Geometry", "toolbar_button_style", static_cast<int>(tb->toolButtonStyle()));
+ }
}
-bool Settings::setNexusApiKey(const QString& apiKey)
+QStringList GeometrySettings::modInfoTabOrder() const
{
- if (!obfuscate("APIKEY", apiKey)) {
- const auto e = GetLastError();
+ QStringList v;
- qCritical().nospace()
- << "Storing API key failed: "
- << formatSystemMessageQ(e);
+ if (m_Settings.contains("mod_info_tabs")) {
+ // old byte array from 2.2.0
+ QDataStream stream(m_Settings.value("mod_info_tabs").toByteArray());
- return false;
+ int count = 0;
+ stream >> count;
+
+ for (int i=0; i<count; ++i) {
+ QString s;
+ stream >> s;
+ v.push_back(s);
+ }
+ } else {
+ // string list since 2.2.1
+ QString string = get<QString>(m_Settings, "Widgets", "ModInfoTabOrder", "");
+ QTextStream stream(&string);
+
+ while (!stream.atEnd()) {
+ QString s;
+ stream >> s;
+ v.push_back(s);
+ }
}
- return true;
+ return v;
}
-bool Settings::clearNexusApiKey()
+void GeometrySettings::setModInfoTabOrder(const QString& names)
{
- return setNexusApiKey("");
+ set(m_Settings, "Widgets", "ModInfoTabOrder", names);
}
-bool Settings::hasNexusApiKey() const
+bool GeometrySettings::centerDialogs() const
{
- return !deObfuscate("APIKEY").isEmpty();
+ return get<bool>(m_Settings, "Settings", "center_dialogs", false);
}
-bool Settings::getSteamLogin(QString &username, QString &password) const
+void GeometrySettings::setCenterDialogs(bool b)
{
- if (m_Settings.contains("Settings/steam_username")) {
- QString tempPass = deObfuscate("steam_password");
- if (!tempPass.isEmpty()) {
- username = m_Settings.value("Settings/steam_username").toString();
- password = tempPass;
- return true;
+ set(m_Settings, "Settings", "center_dialogs", b);
+}
+
+void GeometrySettings::centerOnMainWindowMonitor(QWidget* w)
+{
+ const auto monitor = getOptional<int>(
+ m_Settings, "Geometry", "MainWindow_monitor").value_or(-1);
+
+ centerOnMonitor(w, monitor);
+}
+
+void GeometrySettings::centerOnMonitor(QWidget* w, int monitor)
+{
+ QPoint center;
+
+ if (monitor >= 0 && monitor < QGuiApplication::screens().size()) {
+ center = QGuiApplication::screens().at(monitor)->geometry().center();
+ } else {
+ center = QGuiApplication::primaryScreen()->geometry().center();
+ }
+
+ w->move(center - w->rect().center());
+}
+
+void GeometrySettings::centerOnParent(QWidget* w, QWidget* parent)
+{
+ if (!parent) {
+ parent = w->parentWidget();
+
+ if (!parent) {
+ parent = qApp->activeWindow();
+ }
+ }
+
+ if (parent && parent->isVisible()) {
+ const auto pr = parent->geometry();
+ w->move(pr.center() - w->rect().center());
+ }
+}
+
+void GeometrySettings::saveMainWindowMonitor(const QMainWindow* w)
+{
+ if (auto* handle=w->windowHandle()) {
+ if (auto* screen = handle->screen()) {
+ const int screenId = QGuiApplication::screens().indexOf(screen);
+ set(m_Settings, "Geometry", "MainWindow_monitor", screenId);
}
}
- return false;
}
-bool Settings::compactDownloads() const
+
+Qt::Orientation dockOrientation(const QMainWindow* mw, const QDockWidget* d)
{
- return m_Settings.value("Settings/compact_downloads", false).toBool();
+ // docks in these areas are horizontal
+ const auto horizontalAreas =
+ Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea;
+
+ if (mw->dockWidgetArea(const_cast<QDockWidget*>(d)) & horizontalAreas) {
+ return Qt::Horizontal;
+ } else {
+ return Qt::Vertical;
+ }
}
-bool Settings::metaDownloads() const
+void GeometrySettings::saveDocks(const QMainWindow* mw)
{
- return m_Settings.value("Settings/meta_downloads", false).toBool();
+ // this attempts to fix https://bugreports.qt.io/browse/QTBUG-46620 where dock
+ // sizes are not restored when the main window is maximized; it is used in
+ // MainWindow::readSettings() and MainWindow::storeSettings()
+ //
+ // there's also https://stackoverflow.com/questions/44005852, which has what
+ // seems to be a popular fix, but it breaks the restored size of the window
+ // by setting it to the desktop's resolution, so that doesn't work
+ //
+ // the only fix I could find is to remember the sizes of the docks and manually
+ // setting them back; saving is straightforward, but restoring is messy
+ //
+ // this also depends on the window being visible before the timer in restore()
+ // is fired and the timer must be processed by application.exec(); therefore,
+ // the splash screen _must_ be closed before readSettings() is called, because
+ // it has its own event loop, which seems to interfere with this
+ //
+ // all of this should become unnecessary when QTBUG-46620 is fixed
+ //
+
+ // saves the size of each dock
+ for (const auto* dock : mw->findChildren<QDockWidget*>()) {
+ int size = 0;
+
+ // save the width for horizontal docks, or the height for vertical
+ if (dockOrientation(mw, dock) == Qt::Horizontal) {
+ size = dock->size().width();
+ } else {
+ size = dock->size().height();
+ }
+
+ set(m_Settings, "Geometry", dockSettingName(dock), size);
+ }
}
-bool Settings::offlineMode() const
+void GeometrySettings::restoreDocks(QMainWindow* mw) const
{
- return m_Settings.value("Settings/offline_mode", false).toBool();
+ struct DockInfo
+ {
+ QDockWidget* d;
+ int size = 0;
+ Qt::Orientation ori;
+ };
+
+ std::vector<DockInfo> dockInfos;
+
+ // for each dock
+ for (auto* dock : mw->findChildren<QDockWidget*>()) {
+ if (auto size=getOptional<int>(m_Settings, "Geometry", dockSettingName(dock))) {
+ // remember this dock, its size and orientation
+ dockInfos.push_back({dock, *size, dockOrientation(mw, dock)});
+ }
+ }
+
+ // the main window must have had time to process the settings from
+ // readSettings() or it seems to override whatever is set here
+ //
+ // some people said a single processEvents() call is enough, but it doesn't
+ // look like it
+ QTimer::singleShot(5, [=] {
+ for (const auto& info : dockInfos) {
+ mw->resizeDocks({info.d}, {info.size}, info.ori);
+ }
+ });
}
-int Settings::logLevel() const
+
+WidgetSettings::WidgetSettings(QSettings& s)
+ : m_Settings(s)
{
- return m_Settings.value("Settings/log_level", static_cast<int>(LogLevel::Info)).toInt();
+ MOBase::QuestionBoxMemory::setCallbacks(
+ [this](auto&& w, auto&& f){ return questionButton(w, f); },
+ [this](auto&& w, auto&& b){ setQuestionWindowButton(w, b); },
+ [this](auto&& w, auto&& f, auto&& b){ setQuestionFileButton(w, f, b); });
}
-int Settings::crashDumpsType() const
+std::optional<int> WidgetSettings::index(const QComboBox* cb) const
{
- return m_Settings.value("Settings/crash_dumps_type", static_cast<int>(CrashDumpsType::Mini)).toInt();
+ return getOptional<int>(m_Settings, "Widgets", indexSettingName(cb));
}
-int Settings::crashDumpsMax() const
+void WidgetSettings::saveIndex(const QComboBox* cb)
{
- return m_Settings.value("Settings/crash_dumps_max", 5).toInt();
+ set(m_Settings, "Widgets", indexSettingName(cb), cb->currentIndex());
}
-QColor Settings::modlistOverwrittenLooseColor() const
+void WidgetSettings::restoreIndex(QComboBox* cb, std::optional<int> def) const
{
- return m_Settings.value("Settings/overwrittenLooseFilesColor", QColor(0, 255, 0, 64)).value<QColor>();
+ if (auto v=getOptional<int>(m_Settings, "Widgets", indexSettingName(cb), def)) {
+ cb->setCurrentIndex(*v);
+ }
}
-QColor Settings::modlistOverwritingLooseColor() const
+std::optional<int> WidgetSettings::index(const QTabWidget* w) const
{
- return m_Settings.value("Settings/overwritingLooseFilesColor", QColor(255, 0, 0, 64)).value<QColor>();
+ return getOptional<int>(m_Settings, "Widgets", indexSettingName(w));
}
-QColor Settings::modlistOverwrittenArchiveColor() const
+void WidgetSettings::saveIndex(const QTabWidget* w)
{
- return m_Settings.value("Settings/overwrittenArchiveFilesColor", QColor(0, 255, 255, 64)).value<QColor>();
+ set(m_Settings, "Widgets", indexSettingName(w), w->currentIndex());
}
-QColor Settings::modlistOverwritingArchiveColor() const
+void WidgetSettings::restoreIndex(QTabWidget* w, std::optional<int> def) const
{
- return m_Settings.value("Settings/overwritingArchiveFilesColor", QColor(255, 0, 255, 64)).value<QColor>();
+ if (auto v=getOptional<int>(m_Settings, "Widgets", indexSettingName(w), def)) {
+ w->setCurrentIndex(*v);
+ }
}
-QColor Settings::modlistContainsPluginColor() const
+std::optional<bool> WidgetSettings::checked(const QAbstractButton* w) const
{
- return m_Settings.value("Settings/containsPluginColor", QColor(0, 0, 255, 64)).value<QColor>();
+ warnIfNotCheckable(w);
+ return getOptional<bool>(m_Settings, "Widgets", checkedSettingName(w));
}
-QColor Settings::pluginListContainedColor() const
+void WidgetSettings::saveChecked(const QAbstractButton* w)
{
- return m_Settings.value("Settings/containedColor", QColor(0, 0, 255, 64)).value<QColor>();
+ warnIfNotCheckable(w);
+ set(m_Settings, "Widgets", checkedSettingName(w), w->isChecked());
}
-QString Settings::executablesBlacklist() const
+void WidgetSettings::restoreChecked(QAbstractButton* w, std::optional<bool> def) const
{
- return m_Settings.value("Settings/executable_blacklist", (
- QStringList()
- << "Chrome.exe"
- << "Firefox.exe"
- << "TSVNCache.exe"
- << "TGitCache.exe"
- << "Steam.exe"
- << "GameOverlayUI.exe"
- << "Discord.exe"
- << "GalaxyClient.exe"
- << "Spotify.exe"
- ).join(";")
- ).toString();
+ warnIfNotCheckable(w);
+
+ if (auto v=getOptional<bool>(m_Settings, "Widgets", checkedSettingName(w), def)) {
+ w->setChecked(*v);
+ }
}
-void Settings::setSteamLogin(QString username, QString password)
+QuestionBoxMemory::Button WidgetSettings::questionButton(
+ const QString& windowName, const QString& filename) const
{
- if (username == "") {
- m_Settings.remove("Settings/steam_username");
- password = "";
- } else {
- m_Settings.setValue("Settings/steam_username", username);
+ const QString sectionName("DialogChoices");
+
+ if (!filename.isEmpty()) {
+ const auto fileSetting = windowName + "/" + filename;
+ if (auto v=getOptional<int>(m_Settings, sectionName, fileSetting)) {
+ return static_cast<QuestionBoxMemory::Button>(*v);
+ }
}
- if (!obfuscate("steam_password", password)) {
- const auto e = GetLastError();
- qCritical().nospace()
- << "Storing or deleting password failed: "
- << formatSystemMessageQ(e);
+
+ if (auto v=getOptional<int>(m_Settings, sectionName, windowName)) {
+ return static_cast<QuestionBoxMemory::Button>(*v);
}
+
+ return QuestionBoxMemory::NoButton;
}
-LoadMechanism::EMechanism Settings::getLoadMechanism() const
+void WidgetSettings::setQuestionWindowButton(
+ const QString& windowName, QuestionBoxMemory::Button button)
{
- switch (m_Settings.value("Settings/load_mechanism").toInt()) {
- case LoadMechanism::LOAD_MODORGANIZER: return LoadMechanism::LOAD_MODORGANIZER;
- case LoadMechanism::LOAD_SCRIPTEXTENDER: return LoadMechanism::LOAD_SCRIPTEXTENDER;
- case LoadMechanism::LOAD_PROXYDLL: return LoadMechanism::LOAD_PROXYDLL;
+ const QString sectionName("DialogChoices");
+
+ if (button == QuestionBoxMemory::NoButton) {
+ remove(m_Settings, sectionName, windowName);
+ } else {
+ set(m_Settings, sectionName, windowName, button);
}
- throw std::runtime_error("invalid load mechanism");
}
+void WidgetSettings::setQuestionFileButton(
+ const QString& windowName, const QString& filename,
+ QuestionBoxMemory::Button button)
+{
+ const QString sectionName("DialogChoices");
+ const QString settingName(windowName + "/" + filename);
+
+ if (button == QuestionBoxMemory::NoButton) {
+ remove(m_Settings, sectionName, settingName);
+ } else {
+ set(m_Settings, sectionName, settingName, button);
+ }
+}
-void Settings::setupLoadMechanism()
+void WidgetSettings::resetQuestionButtons()
{
- m_LoadMechanism.activate(getLoadMechanism());
+ removeSection(m_Settings, "DialogChoices");
}
-bool Settings::useProxy() const
+ColorSettings::ColorSettings(QSettings& s)
+ : m_Settings(s)
{
- return m_Settings.value("Settings/use_proxy", false).toBool();
}
-bool Settings::endorsementIntegration() const
+QColor ColorSettings::modlistOverwrittenLoose() const
{
- return m_Settings.value("Settings/endorsement_integration", true).toBool();
+ return get<QColor>(
+ m_Settings, "Settings", "overwrittenLooseFilesColor",
+ QColor(0, 255, 0, 64));
}
-bool Settings::hideAPICounter() const
+void ColorSettings::setModlistOverwrittenLoose(const QColor& c)
{
- return m_Settings.value("Settings/hide_api_counter", false).toBool();
+ set(m_Settings, "Settings", "overwrittenLooseFilesColor", c);
}
-bool Settings::displayForeign() const
+QColor ColorSettings::modlistOverwritingLoose() const
{
- return m_Settings.value("Settings/display_foreign", true).toBool();
+ return get<QColor>(
+ m_Settings, "Settings", "overwritingLooseFilesColor",
+ QColor(255, 0, 0, 64));
}
-void Settings::setMotDHash(uint hash)
+void ColorSettings::setModlistOverwritingLoose(const QColor& c)
{
- m_Settings.setValue("motd_hash", hash);
+ set(m_Settings, "Settings", "overwritingLooseFilesColor", c);
}
-uint Settings::getMotDHash() const
+QColor ColorSettings::modlistOverwrittenArchive() const
{
- return m_Settings.value("motd_hash", 0).toUInt();
+ return get<QColor>(
+ m_Settings, "Settings", "overwrittenArchiveFilesColor",
+ QColor(0, 255, 255, 64));
}
-bool Settings::archiveParsing() const
+void ColorSettings::setModlistOverwrittenArchive(const QColor& c)
{
- return m_Settings.value("Settings/archive_parsing_experimental", false).toBool();
+ set(m_Settings, "Settings", "overwrittenArchiveFilesColor", c);
}
-QVariant Settings::pluginSetting(const QString &pluginName, const QString &key) const
+QColor ColorSettings::modlistOverwritingArchive() const
+{
+ return get<QColor>(
+ m_Settings, "Settings", "overwritingArchiveFilesColor",
+ QColor(255, 0, 255, 64));
+}
+
+void ColorSettings::setModlistOverwritingArchive(const QColor& c)
+{
+ set(m_Settings, "Settings", "overwritingArchiveFilesColor", c);
+}
+
+QColor ColorSettings::modlistContainsPlugin() const
+{
+ return get<QColor>(
+ m_Settings, "Settings", "containsPluginColor",
+ QColor(0, 0, 255, 64));
+}
+
+void ColorSettings::setModlistContainsPlugin(const QColor& c)
+{
+ set(m_Settings, "Settings", "containsPluginColor", c);
+}
+
+QColor ColorSettings::pluginListContained() const
+{
+ return get<QColor>(
+ m_Settings, "Settings", "containedColor",
+ QColor(0, 0, 255, 64));
+}
+
+void ColorSettings::setPluginListContained(const QColor& c)
+{
+ set(m_Settings, "Settings", "containedColor", c);
+}
+
+std::optional<QColor> ColorSettings::previousSeparatorColor() const
+{
+ const auto c = getOptional<QColor>(m_Settings, "General", "previousSeparatorColor");
+ if (c && c->isValid()) {
+ return c;
+ }
+
+ return {};
+}
+
+void ColorSettings::setPreviousSeparatorColor(const QColor& c) const
+{
+ set(m_Settings, "General", "previousSeparatorColor", c);
+}
+
+void ColorSettings::removePreviousSeparatorColor()
+{
+ remove(m_Settings, "General", "previousSeparatorColor");
+}
+
+bool ColorSettings::colorSeparatorScrollbar() const
+{
+ return get<bool>(m_Settings, "Settings", "colorSeparatorScrollbars", true);
+}
+
+void ColorSettings::setColorSeparatorScrollbar(bool b)
+{
+ set(m_Settings, "Settings", "colorSeparatorScrollbars", b);
+}
+
+QColor ColorSettings::idealTextColor(const QColor& rBackgroundColor)
+{
+ if (rBackgroundColor.alpha() == 0)
+ return QColor(Qt::black);
+
+ const int THRESHOLD = 106 * 255.0f / rBackgroundColor.alpha();
+ int BackgroundDelta = (rBackgroundColor.red() * 0.299) + (rBackgroundColor.green() * 0.587) + (rBackgroundColor.blue() * 0.114);
+ return QColor((255 - BackgroundDelta <= THRESHOLD) ? Qt::black : Qt::white);
+}
+
+
+
+PluginSettings::PluginSettings(QSettings& settings)
+ : m_Settings(settings)
+{
+}
+
+void PluginSettings::clearPlugins()
+{
+ m_Plugins.clear();
+ m_PluginSettings.clear();
+ m_PluginBlacklist.clear();
+
+ m_PluginBlacklist = readBlacklist();
+}
+
+void PluginSettings::registerPlugin(IPlugin *plugin)
+{
+ m_Plugins.push_back(plugin);
+ m_PluginSettings.insert(plugin->name(), QVariantMap());
+ m_PluginDescriptions.insert(plugin->name(), QVariantMap());
+
+ for (const PluginSetting &setting : plugin->settings()) {
+ const QString settingName = plugin->name() + "/" + setting.key;
+
+ QVariant temp = get<QVariant>(
+ m_Settings, "Plugins", settingName, setting.defaultValue);
+
+ if (!temp.convert(setting.defaultValue.type())) {
+ log::warn(
+ "failed to interpret \"{}\" as correct type for \"{}\" in plugin \"{}\", using default",
+ temp.toString(), setting.key, plugin->name());
+
+ temp = setting.defaultValue;
+ }
+
+ m_PluginSettings[plugin->name()][setting.key] = temp;
+
+ m_PluginDescriptions[plugin->name()][setting.key] = QString("%1 (default: %2)")
+ .arg(setting.description)
+ .arg(setting.defaultValue.toString());
+ }
+}
+
+std::vector<MOBase::IPlugin*> PluginSettings::plugins() const
+{
+ return m_Plugins;
+}
+
+QVariant PluginSettings::setting(const QString &pluginName, const QString &key) const
{
auto iterPlugin = m_PluginSettings.find(pluginName);
if (iterPlugin == m_PluginSettings.end()) {
return QVariant();
}
+
auto iterSetting = iterPlugin->find(key);
if (iterSetting == iterPlugin->end()) {
return QVariant();
@@ -559,677 +1279,763 @@ QVariant Settings::pluginSetting(const QString &pluginName, const QString &key)
return *iterSetting;
}
-void Settings::setPluginSetting(const QString &pluginName, const QString &key, const QVariant &value)
+void PluginSettings::setSetting(const QString &pluginName, const QString &key, const QVariant &value)
{
auto iterPlugin = m_PluginSettings.find(pluginName);
+
if (iterPlugin == m_PluginSettings.end()) {
- throw MyException(tr("attempt to store setting for unknown plugin \"%1\"").arg(pluginName));
+ throw MyException(
+ QObject::tr("attempt to store setting for unknown plugin \"%1\"")
+ .arg(pluginName));
}
// store the new setting both in memory and in the ini
m_PluginSettings[pluginName][key] = value;
- m_Settings.setValue("Plugins/" + pluginName + "/" + key, value);
+ set(m_Settings, "Plugins", pluginName + "/" + key, value);
+}
+
+QVariantMap PluginSettings::settings(const QString &pluginName) const
+{
+ return m_PluginSettings[pluginName];
+}
+
+void PluginSettings::setSettings(const QString &pluginName, const QVariantMap& map)
+{
+ m_PluginSettings[pluginName] = map;
+}
+
+QVariantMap PluginSettings::descriptions(const QString &pluginName) const
+{
+ return m_PluginDescriptions[pluginName];
+}
+
+void PluginSettings::setDescriptions(const QString &pluginName, const QVariantMap& map)
+{
+ m_PluginDescriptions[pluginName] = map;
}
-QVariant Settings::pluginPersistent(const QString &pluginName, const QString &key, const QVariant &def) const
+QVariant PluginSettings::persistent(const QString &pluginName, const QString &key, const QVariant &def) const
{
if (!m_PluginSettings.contains(pluginName)) {
return def;
}
- return m_Settings.value("PluginPersistance/" + pluginName + "/" + key, def);
+
+ return get<QVariant>(m_Settings, "PluginPersistance", pluginName + "/" + key, def);
}
-void Settings::setPluginPersistent(const QString &pluginName, const QString &key, const QVariant &value, bool sync)
+void PluginSettings::setPersistent(
+ const QString &pluginName, const QString &key, const QVariant &value, bool sync)
{
if (!m_PluginSettings.contains(pluginName)) {
- throw MyException(tr("attempt to store setting for unknown plugin \"%1\"").arg(pluginName));
+ throw MyException(
+ QObject::tr("attempt to store setting for unknown plugin \"%1\"")
+ .arg(pluginName));
}
- m_Settings.setValue("PluginPersistance/" + pluginName + "/" + key, value);
+
+ set(m_Settings, "PluginPersistance", pluginName + "/" + key, value);
+
if (sync) {
m_Settings.sync();
}
}
-QString Settings::language()
+void PluginSettings::addBlacklist(const QString &fileName)
{
- QString result = m_Settings.value("Settings/language", "").toString();
- if (result.isEmpty()) {
- QStringList languagePreferences = QLocale::system().uiLanguages();
- if (languagePreferences.length() > 0) {
- // the users most favoritest language
- result = languagePreferences.at(0);
- } else {
- // fallback system locale
- result = QLocale::system().name();
- }
- }
- return result;
+ m_PluginBlacklist.insert(fileName);
+ writeBlacklist();
}
-void Settings::updateServers(const QList<ServerInfo> &servers)
+bool PluginSettings::blacklisted(const QString &fileName) const
{
- m_Settings.beginGroup("Servers");
- QStringList oldServerKeys = m_Settings.childKeys();
+ return m_PluginBlacklist.contains(fileName);
+}
- for (const ServerInfo &server : servers) {
- if (!oldServerKeys.contains(server.name)) {
- // not yet known server
- QVariantMap newVal;
- newVal["premium"] = server.premium;
- newVal["preferred"] = server.preferred ? 1 : 0;
- newVal["lastSeen"] = server.lastSeen;
- newVal["downloadCount"] = 0;
- newVal["downloadSpeed"] = 0.0;
+void PluginSettings::setBlacklist(const QStringList& pluginNames)
+{
+ m_PluginBlacklist.clear();
- m_Settings.setValue(server.name, newVal);
- } else {
- QVariantMap data = m_Settings.value(server.name).toMap();
- data["lastSeen"] = server.lastSeen;
- data["premium"] = server.premium;
+ for (const auto& name : pluginNames) {
+ m_PluginBlacklist.insert(name);
+ }
+}
+
+const QSet<QString>& PluginSettings::blacklist() const
+{
+ return m_PluginBlacklist;
+}
- m_Settings.setValue(server.name, data);
+void PluginSettings::save()
+{
+ for (auto iterPlugins=m_PluginSettings.begin(); iterPlugins!=m_PluginSettings.end(); ++iterPlugins) {
+ for (auto iterSettings=iterPlugins->begin(); iterSettings!=iterPlugins->end(); ++iterSettings) {
+ const auto key = iterPlugins.key() + "/" + iterSettings.key();
+ set(m_Settings, "Plugins", key, iterSettings.value());
}
}
- // clean up unavailable servers
- QDate now = QDate::currentDate();
- for (const QString &key : m_Settings.childKeys()) {
- QVariantMap val = m_Settings.value(key).toMap();
- QDate lastSeen = val["lastSeen"].toDate();
- if (lastSeen.daysTo(now) > 30) {
- qDebug("removing server %s since it hasn't been available for downloads in over a month", qUtf8Printable(key));
- m_Settings.remove(key);
- }
+ writeBlacklist();
+}
+
+void PluginSettings::writeBlacklist()
+{
+ const auto current = readBlacklist();
+
+ if (current.size() > m_PluginBlacklist.size()) {
+ // Qt can't remove array elements, the section must be cleared
+ removeSection(m_Settings, "pluginBlacklist");
}
- m_Settings.endGroup();
+ ScopedWriteArray swa(m_Settings, "pluginBlacklist", m_PluginBlacklist.size());
- m_Settings.sync();
+ for (const QString &plugin : m_PluginBlacklist) {
+ swa.next();
+ swa.set("name", plugin);
+ }
}
-void Settings::addBlacklistPlugin(const QString &fileName)
+QSet<QString> PluginSettings::readBlacklist() const
{
- m_PluginBlacklist.insert(fileName);
- writePluginBlacklist();
+ QSet<QString> set;
+
+ ScopedReadArray sra(m_Settings, "pluginBlacklist");
+ sra.for_each([&]{
+ set.insert(sra.get<QString>("name"));
+ });
+
+ return set;
}
-void Settings::writePluginBlacklist()
-{
- m_Settings.remove("pluginBlacklist");
- m_Settings.beginWriteArray("pluginBlacklist");
- int idx = 0;
- for (const QString &plugin : m_PluginBlacklist) {
- m_Settings.setArrayIndex(idx++);
- m_Settings.setValue("name", plugin);
- }
- m_Settings.endArray();
+PathSettings::PathSettings(QSettings& settings)
+ : m_Settings(settings)
+{
}
-void Settings::addLanguages(QComboBox *languageBox)
+std::map<QString, QString> PathSettings::recent() const
{
- std::vector<std::pair<QString, QString>> languages;
+ std::map<QString, QString> map;
- QDirIterator langIter(QCoreApplication::applicationDirPath() + "/translations", QDir::Files);
- QString pattern = ToQString(AppConfig::translationPrefix()) + "_([a-z]{2,3}(_[A-Z]{2,2})?).qm";
- QRegExp exp(pattern);
- while (langIter.hasNext()) {
- langIter.next();
- QString file = langIter.fileName();
- if (exp.exactMatch(file)) {
- QString languageCode = exp.cap(1);
- QLocale locale(languageCode);
- QString languageString = QString("%1 (%2)").arg(locale.nativeLanguageName()).arg(locale.nativeCountryName()); //QLocale::languageToString(locale.language());
- if (locale.language() == QLocale::Chinese) {
- if (languageCode == "zh_TW") {
- languageString = "Chinese (traditional)";
- } else {
- languageString = "Chinese (simplified)";
- }
- }
- languages.push_back(std::make_pair(QString("%1").arg(languageString), exp.cap(1)));
- //languageBox->addItem(QString("%1").arg(languageString), exp.cap(1));
+ ScopedReadArray sra(m_Settings, "recentDirectories");
+
+ sra.for_each([&] {
+ const QVariant name = sra.get<QVariant>("name");
+ const QVariant dir = sra.get<QVariant>("directory");
+
+ if (name.isValid() && dir.isValid()) {
+ map.emplace(name.toString(), dir.toString());
}
+ });
+
+ return map;
+}
+
+void PathSettings::setRecent(const std::map<QString, QString>& map)
+{
+ const auto current = recent();
+
+ if (current.size() > map.size()) {
+ // Qt can't remove array elements, the section must be cleared
+ removeSection(m_Settings, "recentDirectories");
}
- if (!languageBox->findText("English")) {
- languages.push_back(std::make_pair(QString("English"), QString("en_US")));
- //languageBox->addItem("English", "en_US");
- }
- std::sort(languages.begin(), languages.end());
- for (const auto &lang : languages) {
- languageBox->addItem(lang.first, lang.second);
+
+ ScopedWriteArray swa(m_Settings, "recentDirectories", map.size());
+
+ for (auto&& p : map) {
+ swa.next();
+
+ swa.set("name", p.first);
+ swa.set("directory", p.second);
}
}
-void Settings::addStyles(QComboBox *styleBox)
+QString PathSettings::getConfigurablePath(const QString &key,
+ const QString &def,
+ bool resolve) const
{
- styleBox->addItem("None", "");
- styleBox->addItem("Fusion", "Fusion");
+ QString result = QDir::fromNativeSeparators(
+ get<QString>(m_Settings, "Settings", key, QString("%BASE_DIR%/") + def));
- QDirIterator langIter(QCoreApplication::applicationDirPath() + "/" + ToQString(AppConfig::stylesheetsPath()), QStringList("*.qss"), QDir::Files);
- while (langIter.hasNext()) {
- langIter.next();
- QString style = langIter.fileName();
- styleBox->addItem(style, style);
+ if (resolve) {
+ result.replace("%BASE_DIR%", base());
+ }
+
+ return result;
+}
+
+void PathSettings::setConfigurablePath(const QString &key, const QString& path)
+{
+ if (path.isEmpty()) {
+ remove(m_Settings, "Settings", key);
+ } else {
+ set(m_Settings, "Settings", key, path);
}
}
-void Settings::resetDialogs()
+QString PathSettings::base() const
+{
+ return QDir::fromNativeSeparators(get<QString>(m_Settings,
+ "Settings", "base_directory", qApp->property("dataPath").toString()));
+}
+
+QString PathSettings::downloads(bool resolve) const
{
- QuestionBoxMemory::resetDialogs();
+ return getConfigurablePath(
+ "download_directory",
+ ToQString(AppConfig::downloadPath()),
+ resolve);
}
-void Settings::query(PluginContainer *pluginContainer, QWidget *parent)
+QString PathSettings::cache(bool resolve) const
{
- SettingsDialog dialog(pluginContainer, this, parent);
- connect(&dialog, SIGNAL(resetDialogs()), this, SLOT(resetDialogs()));
+ return getConfigurablePath(
+ "cache_directory",
+ ToQString(AppConfig::cachePath()),
+ resolve);
+}
- std::vector<std::unique_ptr<SettingsTab>> tabs;
+QString PathSettings::mods(bool resolve) const
+{
+ return getConfigurablePath(
+ "mod_directory",
+ ToQString(AppConfig::modsPath()),
+ resolve);
+}
- tabs.push_back(std::unique_ptr<SettingsTab>(new GeneralTab(this, dialog)));
- tabs.push_back(std::unique_ptr<SettingsTab>(new PathsTab(this, dialog)));
- tabs.push_back(std::unique_ptr<SettingsTab>(new DiagnosticsTab(this, dialog)));
- tabs.push_back(std::unique_ptr<SettingsTab>(new NexusTab(this, dialog)));
- tabs.push_back(std::unique_ptr<SettingsTab>(new SteamTab(this, dialog)));
- tabs.push_back(std::unique_ptr<SettingsTab>(new PluginsTab(this, dialog)));
- tabs.push_back(std::unique_ptr<SettingsTab>(new WorkaroundsTab(this, dialog)));
+QString PathSettings::profiles(bool resolve) const
+{
+ return getConfigurablePath(
+ "profiles_directory",
+ ToQString(AppConfig::profilesPath()),
+ resolve);
+}
+QString PathSettings::overwrite(bool resolve) const
+{
+ return getConfigurablePath(
+ "overwrite_directory",
+ ToQString(AppConfig::overwritePath()),
+ resolve);
+}
- QString key = QString("geometry/%1").arg(dialog.objectName());
- if (m_Settings.contains(key)) {
- dialog.restoreGeometry(m_Settings.value(key).toByteArray());
+void PathSettings::setBase(const QString& path)
+{
+ if (path.isEmpty()) {
+ remove(m_Settings, "Settings", "base_directory");
+ } else {
+ set(m_Settings, "Settings", "base_directory", path);
}
+}
+
+void PathSettings::setDownloads(const QString& path)
+{
+ setConfigurablePath("download_directory", path);
+}
+
+void PathSettings::setMods(const QString& path)
+{
+ setConfigurablePath("mod_directory", path);
+}
+
+void PathSettings::setCache(const QString& path)
+{
+ setConfigurablePath("cache_directory", path);
+}
+
+void PathSettings::setProfiles(const QString& path)
+{
+ setConfigurablePath("profiles_directory", path);
+}
+
+void PathSettings::setOverwrite(const QString& path)
+{
+ setConfigurablePath("overwrite_directory", path);
+}
+
+
+NetworkSettings::NetworkSettings(QSettings& settings)
+ : m_Settings(settings)
+{
+}
+
+bool NetworkSettings::offlineMode() const
+{
+ return get<bool>(m_Settings, "Settings", "offline_mode", false);
+}
+
+void NetworkSettings::setOfflineMode(bool b)
+{
+ set(m_Settings, "Settings", "offline_mode", b);
+}
+
+bool NetworkSettings::useProxy() const
+{
+ return get<bool>(m_Settings, "Settings", "use_proxy", false);
+}
- if (dialog.exec() == QDialog::Accepted) {
- // remember settings before change
- QMap<QString, QString> before;
- m_Settings.beginGroup("Settings");
- for (auto k : m_Settings.allKeys())
- before[k] = m_Settings.value(k).toString();
- m_Settings.endGroup();
+void NetworkSettings::setUseProxy(bool b)
+{
+ set(m_Settings, "Settings", "use_proxy", b);
+}
- // transfer modified settings to configuration file
- for (std::unique_ptr<SettingsTab> const &tab: tabs) {
- tab->update();
+void NetworkSettings::setDownloadSpeed(const QString& name, int bytesPerSecond)
+{
+ auto current = servers();
+
+ for (auto& server : current) {
+ if (server.name() == name) {
+ server.addDownload(bytesPerSecond);
+ updateServers(current);
+ return;
}
+ }
+
+ log::error(
+ "server '{}' not found while trying to add a download with bps {}",
+ name, bytesPerSecond);
+}
+
+ServerList NetworkSettings::servers() const
+{
+ ServerList list;
+
+ {
+ ScopedReadArray sra(m_Settings, "Servers");
+
+ sra.for_each([&] {
+ ServerInfo::SpeedList lastDownloads;
- // print "changed" settings
- m_Settings.beginGroup("Settings");
- bool first_update = true;
- for (auto k : m_Settings.allKeys())
- if (m_Settings.value(k).toString() != before[k] && !k.contains("username") && !k.contains("password"))
- {
- if (first_update) {
- qDebug("Changed settings:");
- first_update = false;
+ const auto lastDownloadsString = sra.get<QString>("lastDownloads", "");
+
+ for (const auto& s : lastDownloadsString.split(" ")) {
+ const auto bytesPerSecond = s.toInt();
+ if (bytesPerSecond > 0) {
+ lastDownloads.push_back(bytesPerSecond);
}
- qDebug(" %s=%s", k.toUtf8().data(), m_Settings.value(k).toString().toUtf8().data());
}
- m_Settings.endGroup();
+
+ ServerInfo server(
+ sra.get<QString>("name", ""),
+ sra.get<bool>("premium", false),
+ QDate::fromString(sra.get<QString>("lastSeen", ""), Qt::ISODate),
+ sra.get<int>("preferred", 0),
+ lastDownloads);
+
+ list.add(std::move(server));
+ });
}
- m_Settings.setValue(key, dialog.saveGeometry());
- // These changes happen regardless of accepted or rejected
- bool restartNeeded = false;
- if (dialog.getApiKeyChanged()) {
- restartNeeded = true;
+ return list;
+}
+
+void NetworkSettings::updateServers(ServerList newServers)
+{
+ // clean up unavailable servers
+ newServers.cleanup();
+
+ const auto current = servers();
+
+ if (current.size() > newServers.size()) {
+ // Qt can't remove array elements, the section must be cleared
+ removeSection(m_Settings, "Servers");
}
- if (dialog.getResetGeometries()) {
- restartNeeded = true;
- m_Settings.setValue("reset_geometry", true);
+
+
+ ScopedWriteArray swa(m_Settings, "Servers", newServers.size());
+
+ for (const auto& server : newServers) {
+ swa.next();
+
+ swa.set("name", server.name());
+ swa.set("premium", server.isPremium());
+ swa.set("lastSeen", server.lastSeen().toString(Qt::ISODate));
+ swa.set("preferred", server.preferred());
+
+ QString lastDownloads;
+ for (const auto& speed : server.lastDownloads()) {
+ if (speed > 0) {
+ lastDownloads += QString("%1 ").arg(speed);
+ }
+ }
+
+ swa.set("lastDownloads", lastDownloads.trimmed());
}
- if (restartNeeded) {
- if (QMessageBox::question(nullptr,
- tr("Restart Mod Organizer?"),
- tr("In order to finish configuration changes, MO must be restarted.\n"
- "Restart it now?"),
- QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) {
- qApp->exit(INT_MAX);
+}
+
+void NetworkSettings::updateFromOldMap()
+{
+ // servers used to be a map of byte arrays until 2.2.1, it's now an array of
+ // individual values instead
+ //
+ // so post 2.2.1, only one key is returned: "size", the size of the arrays;
+ // in 2.2.1, one key per server is returned
+
+ // sanity check that this is really 2.2.1
+ {
+ const QStringList keys = ScopedGroup(m_Settings, "Servers").keys();
+
+ for (auto&& k : keys) {
+ if (k == "size") {
+ // this looks like an array, so the upgrade was probably already done
+ return;
+ }
}
}
+ const auto servers = serversFromOldMap();
+ removeSection(m_Settings, "Servers");
+ updateServers(servers);
}
-Settings::SettingsTab::SettingsTab(Settings *m_parent, SettingsDialog &m_dialog)
- : m_parent(m_parent)
- , m_Settings(m_parent->m_Settings)
- , m_dialog(m_dialog)
+ServerList NetworkSettings::serversFromOldMap() const
{
-}
+ // for 2.2.1 and before
+
+ ServerList list;
+ const ScopedGroup sg(m_Settings, "Servers");
+
+ sg.for_each([&](auto&& serverKey) {
+ QVariantMap data = sg.get<QVariantMap>(serverKey);
+
+ ServerInfo server(
+ serverKey,
+ data["premium"].toBool(),
+ data["lastSeen"].toDate(),
+ data["preferred"].toInt(),
+ {});
+
+ // ignoring download count and speed, it's now a list of values instead of
+ // a total
+
+ list.add(std::move(server));
+ });
-Settings::SettingsTab::~SettingsTab()
-{}
+ return list;
+}
-Settings::GeneralTab::GeneralTab(Settings *m_parent, SettingsDialog &m_dialog)
- : Settings::SettingsTab(m_parent, m_dialog)
- , m_languageBox(m_dialog.findChild<QComboBox *>("languageBox"))
- , m_styleBox(m_dialog.findChild<QComboBox *>("styleBox"))
- , m_compactBox(m_dialog.findChild<QCheckBox *>("compactBox"))
- , m_showMetaBox(m_dialog.findChild<QCheckBox *>("showMetaBox"))
- , m_usePrereleaseBox(m_dialog.findChild<QCheckBox *>("usePrereleaseBox"))
- , m_overwritingBtn(m_dialog.findChild<QPushButton *>("overwritingBtn"))
- , m_overwrittenBtn(m_dialog.findChild<QPushButton *>("overwrittenBtn"))
- , m_overwritingArchiveBtn(m_dialog.findChild<QPushButton *>("overwritingArchiveBtn"))
- , m_overwrittenArchiveBtn(m_dialog.findChild<QPushButton *>("overwrittenArchiveBtn"))
- , m_containsBtn(m_dialog.findChild<QPushButton *>("containsBtn"))
- , m_containedBtn(m_dialog.findChild<QPushButton *>("containedBtn"))
- , m_colorSeparatorsBox(m_dialog.findChild<QCheckBox *>("colorSeparatorsBox"))
+void NetworkSettings::dump() const
{
- // FIXME I think 'addLanguages' lives in here not in parent
- m_parent->addLanguages(m_languageBox);
- {
- QString languageCode = m_parent->language();
- int currentID = m_languageBox->findData(languageCode);
- // I made a mess. :( Most languages are stored with only the iso country
- // code (2 characters like "de") but chinese
- // with the exact language variant (zh_TW) so I have to search for both
- // variants
- if (currentID == -1) {
- currentID = m_languageBox->findData(languageCode.mid(0, 2));
- }
- if (currentID != -1) {
- m_languageBox->setCurrentIndex(currentID);
- }
- }
+ log::debug("servers:");
- // FIXME I think addStyles lives in here not in parent
- m_parent->addStyles(m_styleBox);
- {
- int currentID = m_styleBox->findData(
- m_Settings.value("Settings/style", "").toString());
- if (currentID != -1) {
- m_styleBox->setCurrentIndex(currentID);
+ for (const auto& server : servers()) {
+ QString lastDownloads;
+ for (auto speed : server.lastDownloads()) {
+ lastDownloads += QString("%1 ").arg(speed);
}
+
+ log::debug(
+ " . {} premium={} lastSeen={} preferred={} lastDownloads={}",
+ server.name(),
+ server.isPremium() ? "yes" : "no",
+ server.lastSeen().toString(Qt::ISODate),
+ server.preferred(),
+ lastDownloads.trimmed());
}
- /* verision using palette only works with fusion theme for some stupid reason...
- m_overwritingBtn->setAutoFillBackground(true);
- m_overwrittenBtn->setAutoFillBackground(true);
- m_containsBtn->setAutoFillBackground(true);
- m_containedBtn->setAutoFillBackground(true);
- m_overwritingBtn->setPalette(QPalette(m_parent->modlistOverwritingLooseColor()));
- m_overwrittenBtn->setPalette(QPalette(m_parent->modlistOverwrittenLooseColor()));
- m_containsBtn->setPalette(QPalette(m_parent->modlistContainsPluginColor()));
- m_containedBtn->setPalette(QPalette(m_parent->pluginListContainedColor()));
- QPalette palette1 = m_overwritingBtn->palette();
- QPalette palette2 = m_overwrittenBtn->palette();
- QPalette palette3 = m_containsBtn->palette();
- QPalette palette4 = m_containedBtn->palette();
- palette1.setColor(QPalette::Background, m_parent->modlistOverwritingLooseColor());
- palette2.setColor(QPalette::Background, m_parent->modlistOverwrittenLooseColor());
- palette3.setColor(QPalette::Background, m_parent->modlistContainsPluginColor());
- palette4.setColor(QPalette::Background, m_parent->pluginListContainedColor());
- m_overwritingBtn->setPalette(palette1);
- m_overwrittenBtn->setPalette(palette2);
- m_containsBtn->setPalette(palette3);
- m_containedBtn->setPalette(palette4);
- */
+}
- //version with stylesheet
- m_dialog.setButtonColor(m_overwritingBtn, m_parent->modlistOverwritingLooseColor());
- m_dialog.setButtonColor(m_overwrittenBtn, m_parent->modlistOverwrittenLooseColor());
- m_dialog.setButtonColor(m_overwritingArchiveBtn, m_parent->modlistOverwritingArchiveColor());
- m_dialog.setButtonColor(m_overwrittenArchiveBtn, m_parent->modlistOverwrittenArchiveColor());
- m_dialog.setButtonColor(m_containsBtn, m_parent->modlistContainsPluginColor());
- m_dialog.setButtonColor(m_containedBtn, m_parent->pluginListContainedColor());
- m_dialog.setOverwritingColor(m_parent->modlistOverwritingLooseColor());
- m_dialog.setOverwrittenColor(m_parent->modlistOverwrittenLooseColor());
- m_dialog.setOverwritingArchiveColor(m_parent->modlistOverwritingArchiveColor());
- m_dialog.setOverwrittenArchiveColor(m_parent->modlistOverwrittenArchiveColor());
- m_dialog.setContainsColor(m_parent->modlistContainsPluginColor());
- m_dialog.setContainedColor(m_parent->pluginListContainedColor());
+NexusSettings::NexusSettings(Settings& parent, QSettings& settings)
+ : m_Parent(parent), m_Settings(settings)
+{
+}
- m_compactBox->setChecked(m_parent->compactDownloads());
- m_showMetaBox->setChecked(m_parent->metaDownloads());
- m_usePrereleaseBox->setChecked(m_parent->usePrereleases());
- m_colorSeparatorsBox->setChecked(m_parent->colorSeparatorScrollbar());
+bool NexusSettings::apiKey(QString& apiKey) const
+{
+ QString tempKey = getWindowsCredential("APIKEY");
+ if (tempKey.isEmpty())
+ return false;
+
+ apiKey = tempKey;
+ return true;
}
-void Settings::GeneralTab::update()
+bool NexusSettings::setApiKey(const QString& apiKey)
{
- QString oldLanguage = m_parent->language();
- QString newLanguage = m_languageBox->itemData(m_languageBox->currentIndex()).toString();
- if (newLanguage != oldLanguage) {
- m_Settings.setValue("Settings/language", newLanguage);
- emit m_parent->languageChanged(newLanguage);
+ if (!setWindowsCredential("APIKEY", apiKey)) {
+ const auto e = GetLastError();
+ log::error("Storing API key failed: {}", formatSystemMessage(e));
+ return false;
}
- QString oldStyle = m_Settings.value("Settings/style", "").toString();
- QString newStyle = m_styleBox->itemData(m_styleBox->currentIndex()).toString();
- if (oldStyle != newStyle) {
- m_Settings.setValue("Settings/style", newStyle);
- emit m_parent->styleChanged(newStyle);
- }
+ return true;
+}
+
+bool NexusSettings::clearApiKey()
+{
+ return setApiKey("");
+}
+
+bool NexusSettings::hasApiKey() const
+{
+ return !getWindowsCredential("APIKEY").isEmpty();
+}
+
+bool NexusSettings::endorsementIntegration() const
+{
+ return get<bool>(m_Settings, "Settings", "endorsement_integration", true);
+}
- m_Settings.setValue("Settings/overwritingLooseFilesColor", m_dialog.getOverwritingColor());
- m_Settings.setValue("Settings/overwrittenLooseFilesColor", m_dialog.getOverwrittenColor());
- m_Settings.setValue("Settings/overwritingArchiveFilesColor", m_dialog.getOverwritingArchiveColor());
- m_Settings.setValue("Settings/overwrittenArchiveFilesColor", m_dialog.getOverwrittenArchiveColor());
- m_Settings.setValue("Settings/containsPluginColor", m_dialog.getContainsColor());
- m_Settings.setValue("Settings/containedColor", m_dialog.getContainedColor());
- m_Settings.setValue("Settings/compact_downloads", m_compactBox->isChecked());
- m_Settings.setValue("Settings/meta_downloads", m_showMetaBox->isChecked());
- m_Settings.setValue("Settings/use_prereleases", m_usePrereleaseBox->isChecked());
- m_Settings.setValue("Settings/colorSeparatorScrollbars", m_colorSeparatorsBox->isChecked());
+void NexusSettings::setEndorsementIntegration(bool b) const
+{
+ set(m_Settings, "Settings", "endorsement_integration", b);
}
-Settings::PathsTab::PathsTab(Settings *parent, SettingsDialog &dialog)
- : SettingsTab(parent, dialog)
- , m_baseDirEdit(m_dialog.findChild<QLineEdit *>("baseDirEdit"))
- , m_downloadDirEdit(m_dialog.findChild<QLineEdit *>("downloadDirEdit"))
- , m_modDirEdit(m_dialog.findChild<QLineEdit *>("modDirEdit"))
- , m_cacheDirEdit(m_dialog.findChild<QLineEdit *>("cacheDirEdit"))
- , m_profilesDirEdit(m_dialog.findChild<QLineEdit *>("profilesDirEdit"))
- , m_overwriteDirEdit(m_dialog.findChild<QLineEdit *>("overwriteDirEdit"))
- , m_managedGameDirEdit(m_dialog.findChild<QLineEdit *>("managedGameDirEdit"))
+EndorsementState NexusSettings::endorsementState() const
{
- m_baseDirEdit->setText(m_parent->getBaseDirectory());
- m_managedGameDirEdit->setText(m_parent->m_GamePlugin->gameDirectory().absoluteFilePath(m_parent->m_GamePlugin->binaryName()));
- QString basePath = parent->getBaseDirectory();
- QDir baseDir(basePath);
- for (const auto &dir : {
- std::make_pair(m_downloadDirEdit, m_parent->getDownloadDirectory(false)),
- std::make_pair(m_modDirEdit, m_parent->getModDirectory(false)),
- std::make_pair(m_cacheDirEdit, m_parent->getCacheDirectory(false)),
- std::make_pair(m_profilesDirEdit, m_parent->getProfileDirectory(false)),
- std::make_pair(m_overwriteDirEdit, m_parent->getOverwriteDirectory(false))
- }) {
- QString storePath = baseDir.relativeFilePath(dir.second);
- storePath = dir.second;
- dir.first->setText(storePath);
+ return endorsementStateFromString(
+ get<QString>(m_Settings, "General", "endorse_state", ""));
+}
+
+void NexusSettings::setEndorsementState(EndorsementState s)
+{
+ const auto v = toString(s);
+
+ if (v.isEmpty()) {
+ remove(m_Settings, "General", "endorse_state");
+ } else {
+ set(m_Settings, "General", "endorse_state", v);
}
}
-void Settings::PathsTab::update()
+void NexusSettings::registerAsNXMHandler(bool force)
{
- typedef std::tuple<QString, QString, std::wstring> Directory;
+ const auto nxmPath = QCoreApplication::applicationDirPath() + "/nxmhandler.exe";
+ const auto executable = QCoreApplication::applicationFilePath();
+
+ QString mode = force ? "forcereg" : "reg";
+ QString parameters = mode + " " + m_Parent.game().plugin()->gameShortName();
+ for (const QString& altGame : m_Parent.game().plugin()->validShortNames()) {
+ parameters += "," + altGame;
+ }
+ parameters += " \"" + executable + "\"";
- QString basePath = m_parent->getBaseDirectory();
+ const auto r = shell::Execute(nxmPath, parameters);
- for (const Directory &dir :{
- Directory{m_downloadDirEdit->text(), "download_directory", AppConfig::downloadPath()},
- Directory{m_cacheDirEdit->text(), "cache_directory", AppConfig::cachePath()},
- Directory{m_modDirEdit->text(), "mod_directory", AppConfig::modsPath()},
- Directory{m_overwriteDirEdit->text(), "overwrite_directory", AppConfig::overwritePath()},
- Directory{m_profilesDirEdit->text(), "profiles_directory", AppConfig::profilesPath()}
- }) {
- QString path, settingsKey;
- std::wstring defaultName;
- std::tie(path, settingsKey, defaultName) = dir;
+ if (!r.success()) {
+ QMessageBox::critical(
+ nullptr, QObject::tr("Failed"),
+ QObject::tr("Failed to start the helper application: %1").arg(r.toString()));
+ }
+}
- settingsKey = QString("Settings/%1").arg(settingsKey);
- QString realPath = path;
- realPath.replace("%BASE_DIR%", m_baseDirEdit->text());
+SteamSettings::SteamSettings(Settings& parent, QSettings& settings)
+ : m_Parent(parent), m_Settings(settings)
+{
+}
- if (!QDir(realPath).exists()) {
- if (!QDir().mkpath(realPath)) {
- QMessageBox::warning(qApp->activeWindow(), tr("Error"),
- tr("Failed to create \"%1\", you may not have the "
- "necessary permission. path remains unchanged.")
- .arg(realPath));
- }
- }
+QString SteamSettings::appID() const
+{
+ return get<QString>(
+ m_Settings, "Settings", "app_id", m_Parent.game().plugin()->steamAPPId());
+}
- if (QFileInfo(realPath)
- != QFileInfo(basePath + "/" + QString::fromStdWString(defaultName))) {
- m_Settings.setValue(settingsKey, path);
- } else {
- m_Settings.remove(settingsKey);
- }
+void SteamSettings::setAppID(const QString& id)
+{
+ if (id.isEmpty()) {
+ remove(m_Settings, "Settings", "app_id");
+ } else {
+ set(m_Settings, "Settings", "app_id", id);
}
+}
+
+bool SteamSettings::login(QString &username, QString &password) const
+{
+ username = get<QString>(m_Settings, "Settings", "steam_username", "");
+ password = getWindowsCredential("steam_password");
- if (QFileInfo(m_baseDirEdit->text()) !=
- QFileInfo(qApp->property("dataPath").toString())) {
- m_Settings.setValue("Settings/base_directory", m_baseDirEdit->text());
+ return !username.isEmpty() && !password.isEmpty();
+}
+
+void SteamSettings::setLogin(QString username, QString password)
+{
+ if (username == "") {
+ remove(m_Settings, "Settings", "steam_username");
+ password = "";
} else {
- m_Settings.remove("Settings/base_directory");
+ set(m_Settings, "Settings", "steam_username", username);
}
- QFileInfo oldGameExe(m_parent->m_GamePlugin->gameDirectory().absoluteFilePath(m_parent->m_GamePlugin->binaryName()));
- QFileInfo newGameExe(m_managedGameDirEdit->text());
- if (oldGameExe != newGameExe) {
- m_Settings.setValue("gamePath", newGameExe.absolutePath());
+ if (!setWindowsCredential("steam_password", password)) {
+ const auto e = GetLastError();
+ log::error("Storing or deleting password failed: {}", formatSystemMessage(e));
}
}
-Settings::DiagnosticsTab::DiagnosticsTab(Settings *m_parent, SettingsDialog &m_dialog)
- : Settings::SettingsTab(m_parent, m_dialog)
- , m_logLevelBox(m_dialog.findChild<QComboBox *>("logLevelBox"))
- , m_dumpsTypeBox(m_dialog.findChild<QComboBox *>("dumpsTypeBox"))
- , m_dumpsMaxEdit(m_dialog.findChild<QSpinBox *>("dumpsMaxEdit"))
- , m_diagnosticsExplainedLabel(m_dialog.findChild<QLabel *>("diagnosticsExplainedLabel"))
+
+InterfaceSettings::InterfaceSettings(QSettings& settings)
+ : m_Settings(settings)
{
- m_logLevelBox->setCurrentIndex(m_parent->logLevel());
- m_dumpsTypeBox->setCurrentIndex(m_parent->crashDumpsType());
- m_dumpsMaxEdit->setValue(m_parent->crashDumpsMax());
- QString logsPath = qApp->property("dataPath").toString()
- + "/" + QString::fromStdWString(AppConfig::logPath());
- m_diagnosticsExplainedLabel->setText(
- m_diagnosticsExplainedLabel->text()
- .replace("LOGS_FULL_PATH", logsPath)
- .replace("LOGS_DIR", QString::fromStdWString(AppConfig::logPath()))
- .replace("DUMPS_FULL_PATH", QString::fromStdWString(OrganizerCore::crashDumpsPath()))
- .replace("DUMPS_DIR", QString::fromStdWString(AppConfig::dumpsDir()))
- );
}
-void Settings::DiagnosticsTab::update()
+bool InterfaceSettings::lockGUI() const
{
- m_Settings.setValue("Settings/log_level", m_logLevelBox->currentIndex());
- m_Settings.setValue("Settings/crash_dumps_type", m_dumpsTypeBox->currentIndex());
- m_Settings.setValue("Settings/crash_dumps_max", m_dumpsMaxEdit->value());
+ return get<bool>(m_Settings, "Settings", "lock_gui", true);
}
-Settings::NexusTab::NexusTab(Settings *parent, SettingsDialog &dialog)
- : Settings::SettingsTab(parent, dialog)
- , m_offlineBox(dialog.findChild<QCheckBox *>("offlineBox"))
- , m_proxyBox(dialog.findChild<QCheckBox *>("proxyBox"))
- , m_knownServersList(dialog.findChild<QListWidget *>("knownServersList"))
- , m_preferredServersList(
- dialog.findChild<QListWidget *>("preferredServersList"))
- , m_endorsementBox(dialog.findChild<QCheckBox *>("endorsementBox"))
- , m_hideAPICounterBox(dialog.findChild<QCheckBox *>("hideAPICounterBox"))
+void InterfaceSettings::setLockGUI(bool b)
{
- m_offlineBox->setChecked(parent->offlineMode());
- m_proxyBox->setChecked(parent->useProxy());
- m_endorsementBox->setChecked(parent->endorsementIntegration());
- m_hideAPICounterBox->setChecked(parent->hideAPICounter());
+ set(m_Settings, "Settings", "lock_gui", b);
+}
- // display server preferences
- m_Settings.beginGroup("Servers");
- for (const QString &key : m_Settings.childKeys()) {
- QVariantMap val = m_Settings.value(key).toMap();
- QString descriptor = key;
- if (!descriptor.compare("CDN", Qt::CaseInsensitive)) {
- descriptor += QStringLiteral(" (automatic)");
- }
- if (val.contains("downloadSpeed") && val.contains("downloadCount") && (val["downloadCount"].toInt() > 0)) {
- int bps = static_cast<int>(val["downloadSpeed"].toDouble() / val["downloadCount"].toInt());
- descriptor += QString(" (%1 kbps)").arg(bps / 1024);
- }
+std::optional<QString> InterfaceSettings::styleName() const
+{
+ return getOptional<QString>(m_Settings, "Settings", "style");
+}
- QListWidgetItem *newItem = new QListWidgetItemEx<int>(descriptor, Qt::UserRole + 1);
+void InterfaceSettings::setStyleName(const QString& name)
+{
+ set(m_Settings, "Settings", "style", name);
+}
- newItem->setData(Qt::UserRole, key);
- newItem->setData(Qt::UserRole + 1, val["preferred"].toInt());
- if (val["preferred"].toInt() > 0) {
- m_preferredServersList->addItem(newItem);
- } else {
- m_knownServersList->addItem(newItem);
- }
- m_preferredServersList->sortItems(Qt::DescendingOrder);
- }
- m_Settings.endGroup();
+bool InterfaceSettings::compactDownloads() const
+{
+ return get<bool>(m_Settings, "Settings", "compact_downloads", false);
}
-void Settings::NexusTab::update()
+void InterfaceSettings::setCompactDownloads(bool b)
{
- /*
- if (m_loginCheckBox->isChecked()) {
- m_Settings.setValue("Settings/nexus_login", true);
- m_Settings.setValue("Settings/nexus_username", m_usernameEdit->text());
- m_Settings.setValue("Settings/nexus_password", obfuscate(m_passwordEdit->text()));
- } else {
- m_Settings.setValue("Settings/nexus_login", false);
- m_Settings.remove("Settings/nexus_username");
- m_Settings.remove("Settings/nexus_password");
- }
- */
- m_Settings.setValue("Settings/offline_mode", m_offlineBox->isChecked());
- m_Settings.setValue("Settings/use_proxy", m_proxyBox->isChecked());
- m_Settings.setValue("Settings/endorsement_integration", m_endorsementBox->isChecked());
- m_Settings.setValue("Settings/hide_api_counter", m_hideAPICounterBox->isChecked());
+ set(m_Settings, "Settings", "compact_downloads", b);
+}
- // store server preference
- m_Settings.beginGroup("Servers");
- for (int i = 0; i < m_knownServersList->count(); ++i) {
- QString key = m_knownServersList->item(i)->data(Qt::UserRole).toString();
- QVariantMap val = m_Settings.value(key).toMap();
- val["preferred"] = 0;
- m_Settings.setValue(key, val);
- }
- int count = m_preferredServersList->count();
- for (int i = 0; i < count; ++i) {
- QString key = m_preferredServersList->item(i)->data(Qt::UserRole).toString();
- QVariantMap val = m_Settings.value(key).toMap();
- val["preferred"] = count - i;
- m_Settings.setValue(key, val);
- }
- m_Settings.endGroup();
+bool InterfaceSettings::metaDownloads() const
+{
+ return get<bool>(m_Settings, "Settings", "meta_downloads", false);
}
-Settings::SteamTab::SteamTab(Settings *m_parent, SettingsDialog &m_dialog)
- : Settings::SettingsTab(m_parent, m_dialog)
- , m_steamUserEdit(m_dialog.findChild<QLineEdit *>("steamUserEdit"))
- , m_steamPassEdit(m_dialog.findChild<QLineEdit *>("steamPassEdit"))
+void InterfaceSettings::setMetaDownloads(bool b)
{
- if (m_Settings.contains("Settings/steam_username")) {
- m_steamUserEdit->setText(m_Settings.value("Settings/steam_username", "").toString());
- QString password = deObfuscate("steam_password");
- if (!password.isEmpty()) {
- m_steamPassEdit->setText(password);
- }
- }
+ set(m_Settings, "Settings", "meta_downloads", b);
}
-void Settings::SteamTab::update()
+bool InterfaceSettings::hideAPICounter() const
{
- //FIXME this should be inlined here?
- m_parent->setSteamLogin(m_steamUserEdit->text(), m_steamPassEdit->text());
+ return get<bool>(m_Settings, "Settings", "hide_api_counter", false);
}
-Settings::PluginsTab::PluginsTab(Settings *m_parent, SettingsDialog &m_dialog)
- : Settings::SettingsTab(m_parent, m_dialog)
- , m_pluginsList(m_dialog.findChild<QListWidget *>("pluginsList"))
- , m_pluginBlacklistList(m_dialog.findChild<QListWidget *>("pluginBlacklist"))
+void InterfaceSettings::setHideAPICounter(bool b)
{
- // display plugin settings
- QSet<QString> handledNames;
- for (IPlugin *plugin : m_parent->m_Plugins) {
- if (handledNames.contains(plugin->name()))
- continue;
- QListWidgetItem *listItem = new QListWidgetItem(plugin->name(), m_pluginsList);
- listItem->setData(Qt::UserRole, QVariant::fromValue((void*)plugin));
- listItem->setData(Qt::UserRole + 1, m_parent->m_PluginSettings[plugin->name()]);
- listItem->setData(Qt::UserRole + 2, m_parent->m_PluginDescriptions[plugin->name()]);
- m_pluginsList->addItem(listItem);
- handledNames.insert(plugin->name());
- }
+ set(m_Settings, "Settings", "hide_api_counter", b);
+}
- // display plugin blacklist
- for (const QString &pluginName : m_parent->m_PluginBlacklist) {
- m_pluginBlacklistList->addItem(pluginName);
- }
+bool InterfaceSettings::displayForeign() const
+{
+ return get<bool>(m_Settings, "Settings", "display_foreign", true);
}
-void Settings::PluginsTab::update()
+void InterfaceSettings::setDisplayForeign(bool b)
{
- // transfer plugin settings to in-memory structure
- for (int i = 0; i < m_pluginsList->count(); ++i) {
- QListWidgetItem *item = m_pluginsList->item(i);
- m_parent->m_PluginSettings[item->text()] = item->data(Qt::UserRole + 1).toMap();
- }
- // store plugin settings on disc
- for (auto iterPlugins = m_parent->m_PluginSettings.begin(); iterPlugins != m_parent->m_PluginSettings.end(); ++iterPlugins) {
- for (auto iterSettings = iterPlugins->begin(); iterSettings != iterPlugins->end(); ++iterSettings) {
- m_Settings.setValue("Plugins/" + iterPlugins.key() + "/" + iterSettings.key(), iterSettings.value());
+ set(m_Settings, "Settings", "display_foreign", b);
+}
+
+QString InterfaceSettings::language()
+{
+ QString result = get<QString>(m_Settings, "Settings", "language", "");
+
+ if (result.isEmpty()) {
+ QStringList languagePreferences = QLocale::system().uiLanguages();
+
+ if (languagePreferences.length() > 0) {
+ // the users most favoritest language
+ result = languagePreferences.at(0);
+ } else {
+ // fallback system locale
+ result = QLocale::system().name();
}
}
- // store plugin blacklist
- m_parent->m_PluginBlacklist.clear();
- for (QListWidgetItem *item : m_pluginBlacklistList->findItems("*", Qt::MatchWildcard)) {
- m_parent->m_PluginBlacklist.insert(item->text());
- }
- m_parent->writePluginBlacklist();
+ return result;
}
-Settings::WorkaroundsTab::WorkaroundsTab(Settings *m_parent,
- SettingsDialog &m_dialog)
- : Settings::SettingsTab(m_parent, m_dialog)
- , m_appIDEdit(m_dialog.findChild<QLineEdit *>("appIDEdit"))
- , m_mechanismBox(m_dialog.findChild<QComboBox *>("mechanismBox"))
- , m_hideUncheckedBox(m_dialog.findChild<QCheckBox *>("hideUncheckedBox"))
- , m_forceEnableBox(m_dialog.findChild<QCheckBox *>("forceEnableBox"))
- , m_displayForeignBox(m_dialog.findChild<QCheckBox *>("displayForeignBox"))
- , m_lockGUIBox(m_dialog.findChild<QCheckBox *>("lockGUIBox"))
- , m_enableArchiveParsingBox(m_dialog.findChild<QCheckBox *>("enableArchiveParsingBox"))
- , m_resetGeometriesBtn(m_dialog.findChild<QPushButton *>("resetGeometryBtn"))
+void InterfaceSettings::setLanguage(const QString& name)
{
- m_appIDEdit->setText(m_parent->getSteamAppID());
+ set(m_Settings, "Settings", "language", name);
+}
- LoadMechanism::EMechanism mechanismID = m_parent->getLoadMechanism();
- int index = 0;
+bool InterfaceSettings::isTutorialCompleted(const QString& windowName) const
+{
+ return get<bool>(m_Settings, "CompletedWindowTutorials", windowName, false);
+}
- if (m_parent->m_LoadMechanism.isDirectLoadingSupported()) {
- m_mechanismBox->addItem(QObject::tr("Mod Organizer"), LoadMechanism::LOAD_MODORGANIZER);
- if (mechanismID == LoadMechanism::LOAD_MODORGANIZER) {
- index = m_mechanismBox->count() - 1;
- }
- }
+void InterfaceSettings::setTutorialCompleted(const QString& windowName, bool b)
+{
+ set(m_Settings, "CompletedWindowTutorials", windowName, b);
+}
- if (m_parent->m_LoadMechanism.isScriptExtenderSupported()) {
- m_mechanismBox->addItem(QObject::tr("Script Extender"), LoadMechanism::LOAD_SCRIPTEXTENDER);
- if (mechanismID == LoadMechanism::LOAD_SCRIPTEXTENDER) {
- index = m_mechanismBox->count() - 1;
- }
- }
+bool InterfaceSettings::showChangeGameConfirmation() const
+{
+ return get<bool>(m_Settings, "Settings", "show_change_game_confirmation", true);
+}
- if (m_parent->m_LoadMechanism.isProxyDLLSupported()) {
- m_mechanismBox->addItem(QObject::tr("Proxy DLL"), LoadMechanism::LOAD_PROXYDLL);
- if (mechanismID == LoadMechanism::LOAD_PROXYDLL) {
- index = m_mechanismBox->count() - 1;
- }
- }
+void InterfaceSettings::setShowChangeGameConfirmation(bool b)
+{
+ set(m_Settings, "Settings", "show_change_game_confirmation", b);
+}
- m_mechanismBox->setCurrentIndex(index);
+bool InterfaceSettings::doubleClicksOpenPreviews() const
+{
+ return get<bool>(m_Settings, "Settings", "double_click_previews", true);
+}
- m_hideUncheckedBox->setChecked(m_parent->hideUncheckedPlugins());
- m_forceEnableBox->setChecked(m_parent->forceEnableCoreFiles());
- m_displayForeignBox->setChecked(m_parent->displayForeign());
- m_lockGUIBox->setChecked(m_parent->lockGUI());
- m_enableArchiveParsingBox->setChecked(m_parent->archiveParsing());
+void InterfaceSettings::setDoubleClicksOpenPreviews(bool b)
+{
+ set(m_Settings, "Settings", "double_click_previews", b);
+}
- m_resetGeometriesBtn->setChecked(m_parent->directInterface().value("reset_geometry", false).toBool());
- m_dialog.setExecutableBlacklist(m_parent->executablesBlacklist());
+DiagnosticsSettings::DiagnosticsSettings(QSettings& settings)
+ : m_Settings(settings)
+{
+}
+log::Levels DiagnosticsSettings::logLevel() const
+{
+ return get<log::Levels>(m_Settings, "Settings", "log_level", log::Levels::Info);
}
-void Settings::WorkaroundsTab::update()
+void DiagnosticsSettings::setLogLevel(log::Levels level)
{
- if (m_appIDEdit->text() != m_parent->m_GamePlugin->steamAPPId()) {
- m_Settings.setValue("Settings/app_id", m_appIDEdit->text());
- } else {
- m_Settings.remove("Settings/app_id");
- }
- m_Settings.setValue("Settings/load_mechanism", m_mechanismBox->itemData(m_mechanismBox->currentIndex()).toInt());
- m_Settings.setValue("Settings/hide_unchecked_plugins", m_hideUncheckedBox->isChecked());
- m_Settings.setValue("Settings/force_enable_core_files", m_forceEnableBox->isChecked());
- m_Settings.setValue("Settings/display_foreign", m_displayForeignBox->isChecked());
- m_Settings.setValue("Settings/lock_gui", m_lockGUIBox->isChecked());
- m_Settings.setValue("Settings/archive_parsing_experimental", m_enableArchiveParsingBox->isChecked());
+ set(m_Settings, "Settings", "log_level", level);
+}
+
+lootcli::LogLevels DiagnosticsSettings::lootLogLevel() const
+{
+ return get<lootcli::LogLevels>(
+ m_Settings, "Settings", "loot_log_level", lootcli::LogLevels::Info);
+}
+
+void DiagnosticsSettings::setLootLogLevel(lootcli::LogLevels level)
+{
+ set(m_Settings, "Settings", "loot_log_level", level);
+}
+
+CrashDumpsType DiagnosticsSettings::crashDumpsType() const
+{
+ return get<CrashDumpsType>(m_Settings,
+ "Settings", "crash_dumps_type", CrashDumpsType::Mini);
+}
+
+void DiagnosticsSettings::setCrashDumpsType(CrashDumpsType type)
+{
+ set(m_Settings, "Settings", "crash_dumps_type", type);
+}
+
+int DiagnosticsSettings::crashDumpsMax() const
+{
+ return get<int>(m_Settings, "Settings", "crash_dumps_max", 5);
+}
+
+void DiagnosticsSettings::setCrashDumpsMax(int n)
+{
+ set(m_Settings, "Settings", "crash_dumps_max", n);
+}
+
+std::chrono::seconds DiagnosticsSettings::spawnDelay() const
+{
+ return std::chrono::seconds(
+ get<int>(m_Settings, "Settings", "spawn_delay", 0));
+}
- m_Settings.setValue("Settings/executable_blacklist", m_dialog.getExecutableBlacklist());
+void DiagnosticsSettings::setSpawnDelay(std::chrono::seconds t)
+{
+ set(m_Settings, "Settings", "spawn_delay", t.count());
}
diff --git a/src/settings.h b/src/settings.h
index bccd1e81..0e5238b1 100644
--- a/src/settings.h
+++ b/src/settings.h
@@ -21,555 +21,795 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#define SETTINGS_H
#include "loadmechanism.h"
+#include <lootcli/lootcli.h>
+#include <questionboxmemory.h>
+#include <log.h>
+#include <usvfsparameters.h>
-#include <QList>
-#include <QMap>
-#include <QObject>
-#include <QPushButton>
-#include <QSet>
-#include <QSettings>
-#include <QString>
-#include <QVariant>
-#include <QColor>
-#include <QMetaType>
-
-#include <QtGlobal> //for uint
-
-#include <map>
-#include <vector>
-
-class QCheckBox;
-class QComboBox;
-class QLineEdit;
-class QSpinBox;
-class QListWidget;
-class QWidget;
-class QLabel;
-class QPushButton;
-
-struct ServerInfo;
+#ifdef interface
+ #undef interface
+#endif
namespace MOBase {
class IPlugin;
class IPluginGame;
+ class ExpanderWidget;
}
-class SettingsDialog;
-class PluginContainer;
+class QSplitter;
-/**
- * manages the settings for Mod Organizer. The settings are not cached
- * inside the class but read/written directly from/to disc
- **/
-class Settings : public QObject
+class ServerList;
+class Settings;
+
+
+
+// setting for the currently managed game
+//
+class GameSettings
+{
+public:
+ GameSettings(QSettings& setting);
+
+ // game plugin
+ //
+ const MOBase::IPluginGame* plugin();
+ void setPlugin(const MOBase::IPluginGame* gamePlugin);
+
+ // whether files of the core game are forced-enabled so the user can't
+ // accidentally disable them
+ //
+ bool forceEnableCoreFiles() const;
+ void setForceEnableCoreFiles(bool b);
+
+ // the directory where the managed game is stored
+ //
+ std::optional<QString> directory() const;
+ void setDirectory(const QString& path);
+
+ // the name of the managed game
+ //
+ std::optional<QString> name() const;
+ void setName(const QString& name);
+
+ // the edition of the managed game
+ //
+ std::optional<QString> edition() const;
+ void setEdition(const QString& name);
+
+ // the current profile name
+ //
+ std::optional<QString> selectedProfileName() const;
+ void setSelectedProfileName(const QString& name);
+
+ // load mechanism type
+ //
+ LoadMechanism::EMechanism loadMechanismType() const;
+ void setLoadMechanism(LoadMechanism::EMechanism m);
+
+ // load mechanism object
+ //
+ const LoadMechanism& loadMechanism() const;
+ LoadMechanism& loadMechanism();
+
+ // whether the user wants unchecked plugins (esp, esm) to be hidden from
+ // the virtual data directory
+ //
+ bool hideUncheckedPlugins() const;
+ void setHideUncheckedPlugins(bool b);
+
+private:
+ QSettings& m_Settings;
+ const MOBase::IPluginGame* m_GamePlugin;
+ LoadMechanism m_LoadMechanism;
+};
+
+
+// geometry settings for various widgets; this should contain any setting that
+// can get invalid through UI changes or when users change display settings
+// (resolution, monitors, etc.); see WidgetSettings for the counterpart
+//
+// all these settings are stored under [Geometry] and get wiped when the
+// "reset geometry settings" button is clicked in the settings
+//
+// saveGeometry(), restoreGeometry(), saveState() and restoreState() call the
+// same functions on the given widget
+//
+class GeometrySettings
{
+public:
+ GeometrySettings(QSettings& s);
+
+ // asks the settings to get reset
+ //
+ // this gets called from the settings dialog and gets picked up in
+ // resetIfNeeded(), called from runApplication() just before exiting
+ //
+ void requestReset();
+ void resetIfNeeded();
+
+
+ void saveGeometry(const QMainWindow* w);
+ bool restoreGeometry(QMainWindow* w) const;
+
+ void saveGeometry(const QDialog* d);
+ bool restoreGeometry(QDialog* d) const;
+
+ void saveState(const QMainWindow* window);
+ bool restoreState(QMainWindow* window) const;
+
+ void saveState(const QHeaderView* header);
+ bool restoreState(QHeaderView* header) const;
+
+ void saveState(const QSplitter* splitter);
+ bool restoreState(QSplitter* splitter) const;
+
+ void saveState(const MOBase::ExpanderWidget* expander);
+ bool restoreState(MOBase::ExpanderWidget* expander) const;
+
+ void saveVisibility(const QWidget* w);
+ bool restoreVisibility(QWidget* w, std::optional<bool> def={}) const;
+
+ void saveToolbars(const QMainWindow* w);
+ void restoreToolbars(QMainWindow* w) const;
- Q_OBJECT
+ void saveDocks(const QMainWindow* w);
+ void restoreDocks(QMainWindow* w) const;
+ // this should be a generic "tab order" setting, but it only happens for the
+ // mod info dialog right now
+ //
+ QStringList modInfoTabOrder() const;
+ void setModInfoTabOrder(const QString& names);
+
+ // whether dialogs should be centered on their parent
+ //
+ bool centerDialogs() const;
+ void setCenterDialogs(bool b);
+
+ // assumes the given widget is a top-level
+ //
+ void centerOnMainWindowMonitor(QWidget* w);
+
+ // saves the monitor number of the given window
+ //
+ void saveMainWindowMonitor(const QMainWindow* w);
+
+private:
+ QSettings& m_Settings;
+ bool m_Reset;
+
+ void saveWindowGeometry(const QWidget* w);
+ bool restoreWindowGeometry(QWidget* w) const;
+
+ void ensureWindowOnScreen(QWidget* w) const;
+ static void centerOnMonitor(QWidget* w, int monitor);
+ static void centerOnParent(QWidget* w, QWidget* parent=nullptr);
+};
+
+
+// widget settings that should stay valid regardless of UI changes or when users
+// change display settings (resolution, monitors, etc.); see GeometrySettings
+// for the counterpart
+//
+class WidgetSettings
+{
public:
+ WidgetSettings(QSettings& s);
- /**
- * @brief constructor
- **/
- Settings(const QSettings &settingsSource);
+ // selected index for a combobox
+ //
+ std::optional<int> index(const QComboBox* cb) const;
+ void saveIndex(const QComboBox* cb);
+ void restoreIndex(QComboBox* cb, std::optional<int> def={}) const;
- virtual ~Settings();
+ // selected tab index for a tab widget
+ //
+ std::optional<int> index(const QTabWidget* w) const;
+ void saveIndex(const QTabWidget* w);
+ void restoreIndex(QTabWidget* w, std::optional<int> def={}) const;
- static Settings &instance();
+ // check state for a checkable button
+ //
+ std::optional<bool> checked(const QAbstractButton* w) const;
+ void saveChecked(const QAbstractButton* w);
+ void restoreChecked(QAbstractButton* w, std::optional<bool> def={}) const;
- /**
- * unregister all plugins from settings
- */
+ // returns the remembered button for a question dialog, or NoButton if the
+ // user hasn't saved the choice
+ //
+ MOBase::QuestionBoxMemory::Button questionButton(
+ const QString& windowName, const QString& filename) const;
+
+ // sets the button to be remembered for the given window
+ //
+ void setQuestionWindowButton(
+ const QString& windowName, MOBase::QuestionBoxMemory::Button button);
+
+ // sets the button to be remembered for the given file
+ //
+ void setQuestionFileButton(
+ const QString& windowName, const QString& filename,
+ MOBase::QuestionBoxMemory::Button choice);
+
+ // wipes all the remembered buttons
+ //
+ void resetQuestionButtons();
+
+private:
+ QSettings& m_Settings;
+};
+
+
+// various color settings
+//
+class ColorSettings
+{
+public:
+ ColorSettings(QSettings& s);
+
+ QColor modlistOverwrittenLoose() const;
+ void setModlistOverwrittenLoose(const QColor& c);
+
+ QColor modlistOverwritingLoose() const;
+ void setModlistOverwritingLoose(const QColor& c);
+
+ QColor modlistOverwrittenArchive() const;
+ void setModlistOverwrittenArchive(const QColor& c);
+
+ QColor modlistOverwritingArchive() const;
+ void setModlistOverwritingArchive(const QColor& c);
+
+ QColor modlistContainsPlugin() const;
+ void setModlistContainsPlugin(const QColor& c);
+
+ QColor pluginListContained() const;
+ void setPluginListContained(const QColor& c) ;
+
+ std::optional<QColor> previousSeparatorColor() const;
+ void setPreviousSeparatorColor(const QColor& c) const;
+ void removePreviousSeparatorColor();
+
+ // whether the scrollbar of the mod list should have colors for custom
+ // separator colors
+ //
+ bool colorSeparatorScrollbar() const;
+ void setColorSeparatorScrollbar(bool b);
+
+ // returns a color with a good contrast for the given background
+ //
+ static QColor idealTextColor(const QColor& rBackgroundColor);
+
+private:
+ QSettings& m_Settings;
+};
+
+
+// settings about plugins
+//
+class PluginSettings
+{
+public:
+ PluginSettings(QSettings& settings);
+
+
+ // forgets all the plugins
+ //
void clearPlugins();
- /**
- * @brief register plugin to be configurable
- * @param plugin the plugin to register
- * @return true if the plugin may be registered, false if it is blacklisted
- */
+ // adds the given plugin to the list and loads all of its settings
+ //
void registerPlugin(MOBase::IPlugin *plugin);
- /**
- * displays a SettingsDialog that allows the user to change settings. If the
- * user accepts the changes, the settings are immediately written
- **/
- void query(PluginContainer *pluginContainer, QWidget *parent);
+ // returns all the registered plugins
+ //
+ std::vector<MOBase::IPlugin*> plugins() const;
- /**
- * set up the settings for the specified plugins
- **/
- void addPluginSettings(const std::vector<MOBase::IPlugin*> &plugins);
- /**
- * @return true if the user wants unchecked plugins (esp, esm) should be hidden from
- * the virtual dat adirectory
- **/
- bool hideUncheckedPlugins() const;
+ // returns the plugin setting for the given key
+ //
+ QVariant setting(const QString &pluginName, const QString &key) const;
- /**
- * @return true if files of the core game are forced-enabled so the user can't accidentally disable them
- */
- bool forceEnableCoreFiles() const;
+ // sets the plugin setting for the given key
+ //
+ void setSetting(const QString &pluginName, const QString &key, const QVariant &value);
- /**
- * @return true if the GUI should be locked when running executables
- */
- bool lockGUI() const;
+ // returns all settings
+ //
+ QVariantMap settings(const QString &pluginName) const;
- /**
- * @brief register download speed
- * @param url complete download url
- * @param bytesPerSecond download size in bytes per second
- */
- void setDownloadSpeed(const QString &serverName, int bytesPerSecond);
+ // overwrites all settings
+ //
+ void setSettings(const QString &pluginName, const QVariantMap& map);
+
+ // returns all descriptions
+ //
+ QVariantMap descriptions(const QString &pluginName) const;
+
+ // overwrites all descriptions
+ //
+ void setDescriptions(const QString &pluginName, const QVariantMap& map);
+
+
+ // ?
+ QVariant persistent(const QString &pluginName, const QString &key, const QVariant &def) const;
+ void setPersistent(const QString &pluginName, const QString &key, const QVariant &value, bool sync);
- /**
- * the steam appid is assigned by the steam platform to each product sold there.
- * The appid may differ between different versions of a game so it may be impossible
- * for Mod Organizer to automatically recognize it, though usually it does
- * @return the steam appid for the game
- **/
- QString getSteamAppID() const;
- /**
- * retrieves the base directory under which the other directories usually
- * reside
- */
- QString getBaseDirectory() const;
+ // adds the given plugin to the blacklist
+ //
+ void addBlacklist(const QString &fileName);
- /**
- * retrieve the directory where downloads are stored (with native separators)
- **/
- QString getDownloadDirectory(bool resolve = true) const;
+ // returns whether the given plugin is blacklisted
+ //
+ bool blacklisted(const QString &fileName) const;
- /**
- * retrieve a sorted list of preferred servers
- */
- std::map<QString, int> getPreferredServers();
+ // overwrites the whole blacklist
+ //
+ void setBlacklist(const QStringList& pluginNames);
- /**
- * retrieve the directory where mods are stored (with native separators)
- **/
- QString getModDirectory(bool resolve = true) const;
+ // returns the blacklist
+ //
+ const QSet<QString>& blacklist() const;
- /**
- * retrieve the directory where the web cache is stored (with native separators)
- **/
- QString getCacheDirectory(bool resolve = true) const;
- /**
- * retrieve the directory where the managed game is stored (with native separators)
- **/
- QString getManagedGameDirectory() const;
+ // commits all the settings to the ini
+ //
+ void save();
- /**
- * retrieve the directory where profiles stored (with native separators)
- **/
- QString getProfileDirectory(bool resolve = true) const;
+private:
+ QSettings& m_Settings;
+ std::vector<MOBase::IPlugin*> m_Plugins;
+ QMap<QString, QVariantMap> m_PluginSettings;
+ QMap<QString, QVariantMap> m_PluginDescriptions;
+ QSet<QString> m_PluginBlacklist;
+
+ // commits the blacklist to the ini
+ //
+ void writeBlacklist();
+
+ // reads the blacklist from the ini
+ //
+ QSet<QString> readBlacklist() const;
+};
+
+
+// paths for the game and various components
+//
+// if the 'resolve' parameter is true, %BASE_DIR% is expanded; it's set to
+// false mostly in the settings dialog
+//
+class PathSettings
+{
+public:
+ PathSettings(QSettings& settings);
- /**
- * retrieve the directory were new files are stored that can't be assigned
- * to a mod (with native separators)
- */
- QString getOverwriteDirectory(bool resolve = true) const;
+ QString base() const;
+ void setBase(const QString& path);
- /**
- * @return true if the user has set up automatic login to nexus
- **/
- bool automaticLoginEnabled() const;
+ QString downloads(bool resolve = true) const;
+ void setDownloads(const QString& path);
- /**
- * @brief retrieve the login information for nexus
- *
- * @param username (out) receives the user name for nexus
- * @param password (out) received the password for nexus
- * @return true if automatic login is active, false otherwise
- **/
- bool getNexusApiKey(QString &apiKey) const;
+ QString mods(bool resolve = true) const;
+ void setMods(const QString& path);
- /**
- * @brief set the nexus login information
- *
- * @param username username
- * @param password password
- */
- bool setNexusApiKey(const QString& apiKey);
+ QString cache(bool resolve = true) const;
+ void setCache(const QString& path);
- /**
- * @brief clears the nexus login information
- */
- bool clearNexusApiKey();
+ QString profiles(bool resolve = true) const;
+ void setProfiles(const QString& path);
- /**
- * @brief returns whether an API key is currently stored
- */
- bool hasNexusApiKey() const;
+ QString overwrite(bool resolve = true) const;
+ void setOverwrite(const QString& path);
- /**
- * @brief retrieve the login information for steam
- *
- * @param username (out) receives the user name for nexus
- * @param password (out) received the password for nexus
- * @return true if a username has been specified, false otherwise
- **/
- bool getSteamLogin(QString &username, QString &password) const;
- /**
- * @return true if the user disabled internet features
- */
+ // map of names to directories, used to remember the last directory used in
+ // various file pickers
+ //
+ std::map<QString, QString> recent() const;
+ void setRecent(const std::map<QString, QString>& map);
+
+private:
+ QSettings& m_Settings;
+
+ QString getConfigurablePath(const QString &key, const QString &def, bool resolve) const;
+ void setConfigurablePath(const QString &key, const QString& path);
+};
+
+
+class NetworkSettings
+{
+public:
+ NetworkSettings(QSettings& settings);
+
+ // whether the user has disabled online features
+ //
bool offlineMode() const;
+ void setOfflineMode(bool b);
- /**
- * @return true if the user chose compact downloads
- */
- bool compactDownloads() const;
+ // whether the user wants to use the system proxy
+ //
+ bool useProxy() const;
+ void setUseProxy(bool b);
- /**
- * @return true if the user chose meta downloads
- */
- bool metaDownloads() const;
+ // add a new download speed to the list for the given server; each server
+ // remembers the last couple of download speeds and displays the average in
+ // the network settings
+ //
+ void setDownloadSpeed(const QString &serverName, int bytesPerSecond);
- /**
- * @return the configured log level
- */
- int logLevel() const;
+ // known servers
+ //
+ ServerList servers() const;
- /**
- * @return the configured crash dumps type
- */
- int crashDumpsType() const;
+ // sets the servers
+ //
+ void updateServers(ServerList servers);
- /**
- * @return the configured crash dumps max
- */
- int crashDumpsMax() const;
+ // for 2.2.1 and before, rewrites the old byte array map to the new format
+ //
+ void updateFromOldMap();
+
+ void dump() const;
- QColor modlistOverwrittenLooseColor() const;
+private:
+ QSettings& m_Settings;
- QColor modlistOverwritingLooseColor() const;
+ // for pre 2.2.1 ini files
+ //
+ ServerList serversFromOldMap() const;
+};
- QColor modlistOverwrittenArchiveColor() const;
- QColor modlistOverwritingArchiveColor() const;
+enum class EndorsementState
+{
+ Accepted = 1,
+ Refused,
+ NoDecision
+};
- QColor modlistContainsPluginColor() const;
+EndorsementState endorsementStateFromString(const QString& s);
+QString toString(EndorsementState s);
- QColor pluginListContainedColor() const;
- QString executablesBlacklist() const;
+class NexusSettings
+{
+public:
+ NexusSettings(Settings& parent, QSettings& settings);
- /**
- * @brief set the steam login information
- *
- * @param username username
- * @param password password
- */
- void setSteamLogin(QString username, QString password);
+ // if the key exists from the credentials store, puts it in `apiKey` and
+ // returns true; otherwise, returns false and leaves `apiKey` untouched
+ //
+ bool apiKey(QString& apiKey) const;
- /**
- * @return the load mechanism to be used
- **/
- LoadMechanism::EMechanism getLoadMechanism() const;
+ // sets the api key in the credentials store, removes it if empty; returns
+ // false on errors
+ //
+ bool setApiKey(const QString& apiKey);
- /**
- * @brief activate the load mechanism selected by the user
- **/
- void setupLoadMechanism();
+ // removes the api key from the credentials store; returns false on errors
+ //
+ bool clearApiKey();
- /**
- * @return true if the user configured the use of a network proxy
- */
- bool useProxy() const;
+ // returns whether an API key is currently stored
+ //
+ bool hasApiKey() const;
- /**
- * @return true if endorsement integration is enabled
- */
+ // returns whether endorsement integration is enabled
+ //
bool endorsementIntegration() const;
+ void setEndorsementIntegration(bool b) const;
- /**
- * @return true if the API counter should be hidden
- */
- bool hideAPICounter() const;
+ // returns the endorsement state of MO itself
+ //
+ EndorsementState endorsementState() const;
+ void setEndorsementState(EndorsementState s);
- /**
- * @return true if the user wants to see non-official plugins installed outside MO in his mod list
- */
- bool displayForeign() const;
+ // registers MO as the handler for nxm links
+ //
+ // if 'force' is true, the registration dialog will be shown even if the user
+ // said earlier not to
+ //
+ void registerAsNXMHandler(bool force);
- /**
- * @brief sets the new motd hash
- **/
- void setMotDHash(uint hash);
+private:
+ Settings& m_Parent;
+ QSettings& m_Settings;
+};
- /**
- * @return true if the user wants to have archives being parsed to show conflicts and contents
- */
- bool archiveParsing() const;
- /**
- * @return hash of the last displayed message of the day
- **/
- uint getMotDHash() const;
+class SteamSettings
+{
+public:
+ SteamSettings(Settings& parent, QSettings& settings);
- /**
- * @brief allows direct access to the wrapped QSettings object
- * @return the wrapped QSettings object
- */
- QSettings &directInterface() { return m_Settings; }
- const QSettings &directInterface() const { return m_Settings; }
+ // the steam appid is assigned by the steam platform to each product sold
+ // there.
+ //
+ // the appid may differ between different versions of a game so it may be
+ // impossible for MO to automatically recognize it, though usually it does
+ //
+ QString appID() const;
+ void setAppID(const QString& id);
- /**
- * @brief retrieve a setting for one of the installed plugins
- * @param pluginName name of the plugin
- * @param key name of the setting to retrieve
- * @return the requested value as a QVariant
- * @note an invalid QVariant is returned if the the plugin/setting is not declared
- */
- QVariant pluginSetting(const QString &pluginName, const QString &key) const;
+ // the steam username is stored in the ini, but the password is in the
+ // windows credentials store; both values are independent and either can be
+ // empty
+ //
+ // if the username exists in the ini, it is assigned to `username`; if not
+ // `username` is set to an empty string
+ //
+ // if the password exists in the credentials store, it is assigned to
+ // `password`; if not, `password` is set to an empty string
+ //
+ // returns whether _both_ the username and password have a value
+ //
+ bool login(QString &username, QString &password) const;
- /**
- * @brief set a setting for one of the installed mods
- * @param pluginName name of the plugin
- * @param key name of the setting to change
- * @param value the new value to set
- * @throw an exception is thrown if pluginName is invalid
- */
- void setPluginSetting(const QString &pluginName, const QString &key, const QVariant &value);
+ // sets the steam login; the username is saved in the ini file and the
+ // password in the credentials store
+ //
+ // if a value is empty, it is removed from its backing store
+ //
+ void setLogin(QString username, QString password);
- /**
- * @brief retrieve a persistent value for a plugin
- * @param pluginName name of the plugin to store data for
- * @param key id of the value to retrieve
- * @param def default value to return if the value is not set
- * @return the requested value
- */
- QVariant pluginPersistent(const QString &pluginName, const QString &key, const QVariant &def) const;
+private:
+ Settings& m_Parent;
+ QSettings& m_Settings;
+};
- /**
- * @brief set a persistent value for a plugin
- * @param pluginName name of the plugin to store data for
- * @param key id of the value to retrieve
- * @param value value to set
- * @throw an exception is thrown if pluginName is invalid
- */
- void setPluginPersistent(const QString &pluginName, const QString &key, const QVariant &value, bool sync);
- /**
- * @return short code of the configured language (corresponding to the translation files)
- */
- QString language();
+class InterfaceSettings
+{
+public:
+ InterfaceSettings(QSettings& settings);
- /**
- * @brief updates the list of known servers
- * @param list of servers from a recent query
- */
- void updateServers(const QList<ServerInfo> &servers);
+ // whether the GUI should be locked when running executables
+ //
+ bool lockGUI() const;
+ void setLockGUI(bool b);
- /**
- * @brief add a plugin that is to be blacklisted
- * @param fileName name of the plugin to blacklist
- */
- void addBlacklistPlugin(const QString &fileName);
+ // filename of the theme
+ //
+ std::optional<QString> styleName() const;
+ void setStyleName(const QString& name);
- /**
- * @brief test if a plugin is blacklisted and shouldn't be loaded
- * @param fileName name of the plugin
- * @return true if the file is blacklisted
- */
- bool pluginBlacklisted(const QString &fileName) const;
+ // whether to show compact downloads
+ //
+ bool compactDownloads() const;
+ void setCompactDownloads(bool b);
- /**
- * @return all loaded MO plugins
- */
- std::vector<MOBase::IPlugin*> plugins() const { return m_Plugins; }
+ // whether to show meta information for downloads
+ //
+ bool metaDownloads() const;
+ void setMetaDownloads(bool b);
- bool usePrereleases() const;
+ // whether the API counter should be hidden
+ //
+ bool hideAPICounter() const;
+ void setHideAPICounter(bool b);
- /**
- * @brief register MO as the handler for nxm links
- * @param force set to true to enforce the registration dialog to show up,
- * even if the user said earlier not to
- */
- void registerAsNXMHandler(bool force);
+ // whether the user wants to see non-official plugins installed outside MO in
+ // the mod list
+ //
+ bool displayForeign() const;
+ void setDisplayForeign(bool b);
- /**
- * @brief color the scrollbar of the mod list for custom separator colors?
- * @return the state of the setting
- */
- bool colorSeparatorScrollbar() const;
+ // short code of the configured language (corresponding to the translation
+ // files)
+ //
+ QString language();
+ void setLanguage(const QString& name);
-public slots:
+ // whether the given tutorial has been completed
+ //
+ bool isTutorialCompleted(const QString& windowName) const;
+ void setTutorialCompleted(const QString& windowName, bool b=true);
- void managedGameChanged(MOBase::IPluginGame const *gamePlugin);
+ // whether to show the confirmation when switching instances
+ //
+ bool showChangeGameConfirmation() const;
+ void setShowChangeGameConfirmation(bool b);
+
+ // whether double-clicks on files should try to open previews first
+ //
+ bool doubleClicksOpenPreviews() const;
+ void setDoubleClicksOpenPreviews(bool b);
+
+private:
+ QSettings& m_Settings;
+};
+
+
+class DiagnosticsSettings
+{
public:
- static QColor getIdealTextColor(const QColor& rBackgroundColor);
+ DiagnosticsSettings(QSettings& settings);
+
+ // log level for both MO and usvfs
+ //
+ MOBase::log::Levels logLevel() const;
+ void setLogLevel(MOBase::log::Levels level);
+
+ // log level for loot
+ lootcli::LogLevels lootLogLevel() const;
+ void setLootLogLevel(lootcli::LogLevels level);
+
+ // crash dump type for both MO and usvfs
+ //
+ CrashDumpsType crashDumpsType() const;
+ void setCrashDumpsType(CrashDumpsType type);
+
+ // maximum number of dump files keps, for both MO and usvfs
+ //
+ int crashDumpsMax() const;
+ void setCrashDumpsMax(int n);
+
+ std::chrono::seconds spawnDelay() const;
+ void setSpawnDelay(std::chrono::seconds t);
+
private:
+ QSettings& m_Settings;
+};
- static bool obfuscate(const QString key, const QString data);
- static QString deObfuscate(const QString key);
- void addLanguages(QComboBox *languageBox);
- void addStyles(QComboBox *styleBox);
- void readPluginBlacklist();
- void writePluginBlacklist();
- QString getConfigurablePath(const QString &key, const QString &def, bool resolve) const;
+// manages the settings for MO; the settings are accessed directly through a
+// QSettings and so are not cached here
+//
+class Settings : public QObject
+{
+ Q_OBJECT;
- class SettingsTab
- {
- public:
- SettingsTab(Settings *m_parent, SettingsDialog &m_dialog);
- virtual ~SettingsTab();
+public:
+ Settings(const QString& path);
+ ~Settings();
- virtual void update() = 0;
+ static Settings &instance();
- protected:
- Settings *m_parent;
- QSettings &m_Settings;
- SettingsDialog &m_dialog;
+ // name of the ini file
+ //
+ QString filename() const;
- };
+ // version of MO stored in the ini; this may be different from the current
+ // version if the user just updated
+ //
+ std::optional<QVersionNumber> version() const;
- /** Display/store the configuration in the 'general' tab of the settings dialogue */
- class GeneralTab : public SettingsTab
- {
- public:
- GeneralTab(Settings *m_parent, SettingsDialog &m_dialog);
+ // updates the settings to bring them up to date
+ //
+ void processUpdates(const QVersionNumber& current, const QVersionNumber& last);
- void update();
+ // whether MO has been started for the first time
+ //
+ bool firstStart() const;
+ void setFirstStart(bool b);
- private:
- QComboBox *m_languageBox;
- QComboBox *m_styleBox;
- QCheckBox *m_compactBox;
- QCheckBox *m_showMetaBox;
- QCheckBox *m_usePrereleaseBox;
- QPushButton *m_overwritingBtn;
- QPushButton *m_overwrittenBtn;
- QPushButton *m_overwritingArchiveBtn;
- QPushButton *m_overwrittenArchiveBtn;
- QPushButton *m_containsBtn;
- QPushButton *m_containedBtn;
- QCheckBox *m_colorSeparatorsBox;
- };
+ // configured executables
+ //
+ std::vector<std::map<QString, QVariant>> executables() const;
+ void setExecutables(const std::vector<std::map<QString, QVariant>>& v);
- class PathsTab : public SettingsTab
- {
- public:
- PathsTab(Settings *parent, SettingsDialog &dialog);
+ // whether to backup existing mods on install
+ //
+ bool keepBackupOnInstall() const;
+ void setKeepBackupOnInstall(bool b);
- void update();
+ // blacklisted executables do not get hooked by usvfs; this list is managed
+ // by MO but given to usvfs when starting an executable
+ //
+ QString executablesBlacklist() const;
+ bool isExecutableBlacklisted(const QString& s) const;
+ void setExecutablesBlacklist(const QString& s);
- private:
- QLineEdit *m_baseDirEdit;
- QLineEdit *m_downloadDirEdit;
- QLineEdit *m_modDirEdit;
- QLineEdit *m_cacheDirEdit;
- QLineEdit *m_profilesDirEdit;
- QLineEdit *m_overwriteDirEdit;
- QLineEdit *m_managedGameDirEdit;
- };
+ // ? looks obsolete, only used by dead code
+ //
+ unsigned int motdHash() const;
+ void setMotdHash(unsigned int hash);
- class DiagnosticsTab : public SettingsTab
- {
- public:
- DiagnosticsTab(Settings *parent, SettingsDialog &dialog);
+ // whether archives should be parsed to show conflicts and contents
+ //
+ bool archiveParsing() const;
+ void setArchiveParsing(bool b);
- void update();
+ // whether the user wants to check for updates
+ //
+ bool checkForUpdates() const;
+ void setCheckForUpdates(bool b);
- private:
- QComboBox *m_logLevelBox;
- QComboBox *m_dumpsTypeBox;
- QSpinBox *m_dumpsMaxEdit;
- QLabel *m_diagnosticsExplainedLabel;
- };
+ // whether the user wants to upgrade to pre-releases
+ //
+ bool usePrereleases() const;
+ void setUsePrereleases(bool b);
- /** Display/store the configuration in the 'nexus' tab of the settings dialogue */
- class NexusTab : public SettingsTab
- {
- public:
- NexusTab(Settings *m_parent, SettingsDialog &m_dialog);
- void update();
- private:
- QCheckBox *m_offlineBox;
- QCheckBox *m_proxyBox;
- QListWidget *m_knownServersList;
- QListWidget *m_preferredServersList;
- QCheckBox *m_endorsementBox;
- QCheckBox *m_hideAPICounterBox;
- };
+ GameSettings& game();
+ const GameSettings& game() const;
- /** Display/store the configuration in the 'steam' tab of the settings dialogue */
- class SteamTab : public SettingsTab
- {
- public:
- SteamTab(Settings *m_parent, SettingsDialog &m_dialog);
+ GeometrySettings& geometry();
+ const GeometrySettings& geometry() const;
- void update();
+ WidgetSettings& widgets();
+ const WidgetSettings& widgets() const;
- private:
- QLineEdit *m_steamUserEdit;
- QLineEdit *m_steamPassEdit;
- };
+ ColorSettings& colors();
+ const ColorSettings& colors() const;
- /** Display/store the configuration in the 'plugins' tab of the settings dialogue */
- class PluginsTab : public SettingsTab
- {
- public:
- PluginsTab(Settings *m_parent, SettingsDialog &m_dialog);
+ PluginSettings& plugins();
+ const PluginSettings& plugins() const;
- void update();
+ PathSettings& paths();
+ const PathSettings& paths() const;
- private:
- QListWidget *m_pluginsList;
- QListWidget *m_pluginBlacklistList;
- };
+ NetworkSettings& network();
+ const NetworkSettings& network() const;
- /** Display/store the configuration in the 'workarounds' tab of the settings dialogue */
- class WorkaroundsTab : public SettingsTab
- {
- public:
- WorkaroundsTab(Settings *m_parent, SettingsDialog &m_dialog);
+ NexusSettings& nexus();
+ const NexusSettings& nexus() const;
- void update();
+ SteamSettings& steam();
+ const SteamSettings& steam() const;
- private:
- QLineEdit *m_appIDEdit;
- QComboBox *m_mechanismBox;
- QCheckBox *m_hideUncheckedBox;
- QCheckBox *m_forceEnableBox;
- QCheckBox *m_displayForeignBox;
- QCheckBox *m_lockGUIBox;
- QCheckBox *m_enableArchiveParsingBox;
- QPushButton *m_resetGeometriesBtn;
- };
+ InterfaceSettings& interface();
+ const InterfaceSettings& interface() const;
-private slots:
+ DiagnosticsSettings& diagnostics();
+ const DiagnosticsSettings& diagnostics() const;
- void resetDialogs();
+ // makes sure the ini file is written to disk
+ //
+ QSettings::Status sync() const;
-signals:
+ void dump() const;
+public slots:
+ // this slot is connected to by various parts of MO
+ //
+ void managedGameChanged(MOBase::IPluginGame const *gamePlugin);
+
+signals:
+ // these are fired from outside the settings, mostly by the settings dialog
+ //
void languageChanged(const QString &newLanguage);
void styleChanged(const QString &newStyle);
private:
-
static Settings *s_Instance;
+ mutable QSettings m_Settings;
- MOBase::IPluginGame const *m_GamePlugin;
-
- QSettings m_Settings;
-
- LoadMechanism m_LoadMechanism;
+ GameSettings m_Game;
+ GeometrySettings m_Geometry;
+ WidgetSettings m_Widgets;
+ ColorSettings m_Colors;
+ PluginSettings m_Plugins;
+ PathSettings m_Paths;
+ NetworkSettings m_Network;
+ NexusSettings m_Nexus;
+ SteamSettings m_Steam;
+ InterfaceSettings m_Interface;
+ DiagnosticsSettings m_Diagnostics;
+};
- std::vector<MOBase::IPlugin*> m_Plugins;
- QMap<QString, QVariantMap> m_PluginSettings;
- QMap<QString, QVariantMap> m_PluginDescriptions;
+// helper class that calls restoreGeometry() in the constructor and
+// saveGeometry() in the destructor
+//
+template <class W>
+class GeometrySaver
+{
+public:
+ GeometrySaver(Settings& s, W* w)
+ : m_settings(s), m_widget(w)
+ {
+ m_settings.geometry().restoreGeometry(m_widget);
+ }
- QSet<QString> m_PluginBlacklist;
+ ~GeometrySaver()
+ {
+ m_settings.geometry().saveGeometry(m_widget);
+ }
+private:
+ Settings& m_settings;
+ W* m_widget;
};
#endif // SETTINGS_H
diff --git a/src/settingsdialog.cpp b/src/settingsdialog.cpp
index 95e4ceb0..daccfba9 100644
--- a/src/settingsdialog.cpp
+++ b/src/settingsdialog.cpp
@@ -18,115 +18,85 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
*/
#include "settingsdialog.h"
-
#include "ui_settingsdialog.h"
-#include "ui_nexusmanualkey.h"
-#include "categoriesdialog.h"
-#include "helper.h"
-#include "noeditdelegate.h"
-#include "iplugingame.h"
-#include "settings.h"
-#include "instancemanager.h"
-#include "nexusinterface.h"
-#include "nxmaccessmanager.h"
-#include "plugincontainer.h"
+#include "settingsdialogdiagnostics.h"
+#include "settingsdialoggeneral.h"
+#include "settingsdialognexus.h"
+#include "settingsdialogpaths.h"
+#include "settingsdialogplugins.h"
+#include "settingsdialogsteam.h"
+#include "settingsdialogworkarounds.h"
-#include <boost/uuid/uuid_generators.hpp>
-#include <boost/uuid/uuid_io.hpp>
+using namespace MOBase;
-#include <QDirIterator>
-#include <QFileDialog>
-#include <QMessageBox>
-#include <QShortcut>
-#include <QColorDialog>
-#include <QInputDialog>
-#include <QJsonDocument>
-#include <QDesktopServices>
+SettingsDialog::SettingsDialog(PluginContainer *pluginContainer, Settings& settings, QWidget *parent)
+ : TutorableDialog("SettingsDialog", parent)
+ , ui(new Ui::SettingsDialog)
+ , m_settings(settings)
+ , m_exit(Exit::None)
+ , m_pluginContainer(pluginContainer)
+{
+ ui->setupUi(this);
-#define WIN32_LEAN_AND_MEAN
-#include <Windows.h>
+ m_tabs.push_back(std::unique_ptr<SettingsTab>(new GeneralSettingsTab(settings, *this)));
+ m_tabs.push_back(std::unique_ptr<SettingsTab>(new PathsSettingsTab(settings, *this)));
+ m_tabs.push_back(std::unique_ptr<SettingsTab>(new DiagnosticsSettingsTab(settings, *this)));
+ m_tabs.push_back(std::unique_ptr<SettingsTab>(new NexusSettingsTab(settings, *this)));
+ m_tabs.push_back(std::unique_ptr<SettingsTab>(new SteamSettingsTab(settings, *this)));
+ m_tabs.push_back(std::unique_ptr<SettingsTab>(new PluginsSettingsTab(settings, *this)));
+ m_tabs.push_back(std::unique_ptr<SettingsTab>(new WorkaroundsSettingsTab(settings, *this)));
+}
+PluginContainer* SettingsDialog::pluginContainer()
+{
+ return m_pluginContainer;
+}
-using namespace MOBase;
+QWidget* SettingsDialog::parentWidgetForDialogs()
+{
+ if (isVisible()) {
+ return this;
+ } else {
+ return parentWidget();
+ }
+}
+void SettingsDialog::setExitNeeded(ExitFlags e)
+{
+ m_exit = e;
+}
-class NexusManualKeyDialog : public QDialog
+ExitFlags SettingsDialog::exitNeeded() const
{
-public:
- NexusManualKeyDialog(QWidget* parent)
- : QDialog(parent), ui(new Ui::NexusManualKeyDialog)
- {
- ui->setupUi(this);
+ return m_exit;
+}
- connect(ui->openBrowser, &QPushButton::clicked, [&]{ openBrowser(); });
- connect(ui->paste, &QPushButton::clicked, [&]{ paste(); });
- connect(ui->clear, &QPushButton::clicked, [&]{ clear(); });
- }
+int SettingsDialog::exec()
+{
+ GeometrySaver gs(m_settings, this);
- void accept() override
- {
- m_key = ui->key->toPlainText();
- QDialog::accept();
- }
+ m_settings.widgets().restoreIndex(ui->tabWidget);
- const QString& key() const
- {
- return m_key;
- }
+ auto ret = TutorableDialog::exec();
- void openBrowser()
- {
- shell::OpenLink(QUrl("https://www.nexusmods.com/users/myaccount?tab=api"));
- }
+ m_settings.widgets().saveIndex(ui->tabWidget);
- void paste()
- {
- const auto text = QApplication::clipboard()->text();
- if (!text.isEmpty()) {
- ui->key->setPlainText(text);
+ if (ret == QDialog::Accepted) {
+ for (auto&& tab : m_tabs) {
+ tab->closing();
}
- }
- void clear()
- {
- ui->key->clear();
+ // update settings for each tab
+ for (std::unique_ptr<SettingsTab> const &tab: m_tabs) {
+ tab->update();
+ }
}
-private:
- std::unique_ptr<Ui::NexusManualKeyDialog> ui;
- QString m_key;
-};
-
-
-SettingsDialog::SettingsDialog(PluginContainer *pluginContainer, Settings* settings, QWidget *parent)
- : TutorableDialog("SettingsDialog", parent)
- , ui(new Ui::SettingsDialog)
- , m_settings(settings)
- , m_PluginContainer(pluginContainer)
- , m_nexusLogin(new QWebSocket)
- , m_KeyReceived(false)
- , m_KeyCleared(false)
- , m_GeometriesReset(false)
-{
- ui->setupUi(this);
- ui->pluginSettingsList->setStyleSheet("QTreeWidget::item {padding-right: 10px;}");
-
- QShortcut *delShortcut
- = new QShortcut(QKeySequence(Qt::Key_Delete), ui->pluginBlacklist);
- connect(delShortcut, SIGNAL(activated()), this, SLOT(deleteBlacklistItem()));
- connect(m_nexusLogin, SIGNAL(connected()), this, SLOT(dispatchLogin()));
- connect(m_nexusLogin, SIGNAL(error(QAbstractSocket::SocketError)), this, SLOT(authError(QAbstractSocket::SocketError)));
- connect(m_nexusLogin, SIGNAL(textMessageReceived(const QString &)), this, SLOT(receiveApiKey(const QString &)));
- connect(m_nexusLogin, SIGNAL(disconnected()), this, SLOT(completeApiConnection()));
- m_loginTimer.callOnTimeout(this, &SettingsDialog::loginPing);
-
- updateNexusButtons();
+ return ret;
}
SettingsDialog::~SettingsDialog()
{
- m_loginTimer.stop();
- m_nexusLogin->close();
disconnect(this);
delete ui;
}
@@ -141,23 +111,6 @@ QString SettingsDialog::getColoredButtonStyleSheet() const
"}");
}
-void SettingsDialog::setButtonColor(QPushButton *button, const QColor &color)
-{
- button->setStyleSheet(
- QString("QPushButton {"
- "background-color: rgba(%1, %2, %3, %4);"
- "color: %5;"
- "border: 1px solid;"
- "padding: 3px;"
- "}")
- .arg(color.red())
- .arg(color.green())
- .arg(color.blue())
- .arg(color.alpha())
- .arg(Settings::getIdealTextColor(color).name())
- );
-};
-
void SettingsDialog::accept()
{
QString newModPath = ui->modDirEdit->text();
@@ -165,9 +118,9 @@ void SettingsDialog::accept()
if ((QDir::fromNativeSeparators(newModPath) !=
QDir::fromNativeSeparators(
- Settings::instance().getModDirectory(true))) &&
+ Settings::instance().paths().mods(true))) &&
(QMessageBox::question(
- nullptr, tr("Confirm"),
+ parentWidgetForDialogs(), tr("Confirm"),
tr("Changing the mod directory affects all your profiles! "
"Mods not present (or named differently) in the new location "
"will be disabled in all profiles. "
@@ -177,494 +130,28 @@ void SettingsDialog::accept()
return;
}
- storeSettings(ui->pluginsList->currentItem());
TutorableDialog::accept();
}
-bool SettingsDialog::getResetGeometries()
-{
- return ui->resetGeometryBtn->isChecked();
-}
-
-bool SettingsDialog::getApiKeyChanged()
-{
- return m_KeyReceived || m_KeyCleared;
-}
-
-void SettingsDialog::on_categoriesBtn_clicked()
-{
- CategoriesDialog dialog(this);
- if (dialog.exec() == QDialog::Accepted) {
- dialog.commitChanges();
- }
-}
-
-void SettingsDialog::on_execBlacklistBtn_clicked()
-{
- bool ok = false;
- QString result = QInputDialog::getMultiLineText(
- this,
- tr("Executables Blacklist"),
- tr("Enter one executable per line to be blacklisted from the virtual file system.\n"
- "Mods and other virtualized files will not be visible to these executables and\n"
- "any executables launched by them.\n\n"
- "Example:\n"
- " Chrome.exe\n"
- " Firefox.exe"),
- m_ExecutableBlacklist.split(";").join("\n"),
- &ok
- );
- if (ok) {
- QStringList blacklist;
- for (auto exec : result.split("\n")) {
- if (exec.trimmed().endsWith(".exe", Qt::CaseInsensitive)) {
- blacklist << exec.trimmed();
- }
- }
- m_ExecutableBlacklist = blacklist.join(";");
- }
-}
-
-void SettingsDialog::on_bsaDateBtn_clicked()
-{
- IPluginGame const *game
- = qApp->property("managed_game").value<IPluginGame *>();
- QDir dir = game->dataDirectory();
-
- Helper::backdateBSAs(qApp->applicationDirPath().toStdWString(),
- dir.absolutePath().toStdWString());
-}
-
-void SettingsDialog::on_browseBaseDirBtn_clicked()
-{
- QString temp = QFileDialog::getExistingDirectory(
- this, tr("Select base directory"), ui->baseDirEdit->text());
- if (!temp.isEmpty()) {
- ui->baseDirEdit->setText(temp);
- }
-}
-
-void SettingsDialog::on_browseDownloadDirBtn_clicked()
-{
- QString searchPath = ui->downloadDirEdit->text();
- searchPath.replace("%BASE_DIR%", ui->baseDirEdit->text());
-
- QString temp = QFileDialog::getExistingDirectory(this, tr("Select download directory"), searchPath);
- if (!temp.isEmpty()) {
- ui->downloadDirEdit->setText(temp);
- }
-}
-
-void SettingsDialog::on_browseModDirBtn_clicked()
-{
- QString searchPath = ui->modDirEdit->text();
- searchPath.replace("%BASE_DIR%", ui->baseDirEdit->text());
-
- QString temp = QFileDialog::getExistingDirectory(this, tr("Select mod directory"), searchPath);
- if (!temp.isEmpty()) {
- ui->modDirEdit->setText(temp);
- }
-}
-
-void SettingsDialog::on_browseCacheDirBtn_clicked()
-{
- QString searchPath = ui->cacheDirEdit->text();
- searchPath.replace("%BASE_DIR%", ui->baseDirEdit->text());
-
- QString temp = QFileDialog::getExistingDirectory(this, tr("Select cache directory"), searchPath);
- if (!temp.isEmpty()) {
- ui->cacheDirEdit->setText(temp);
- }
-}
-
-void SettingsDialog::on_browseProfilesDirBtn_clicked()
-{
- QString searchPath = ui->profilesDirEdit->text();
- searchPath.replace("%BASE_DIR%", ui->baseDirEdit->text());
-
- QString temp = QFileDialog::getExistingDirectory(this, tr("Select profiles directory"), searchPath);
- if (!temp.isEmpty()) {
- ui->profilesDirEdit->setText(temp);
- }
-}
-
-void SettingsDialog::on_browseOverwriteDirBtn_clicked()
-{
- QString searchPath = ui->overwriteDirEdit->text();
- searchPath.replace("%BASE_DIR%", ui->baseDirEdit->text());
-
- QString temp = QFileDialog::getExistingDirectory(this, tr("Select overwrite directory"), searchPath);
- if (!temp.isEmpty()) {
- ui->overwriteDirEdit->setText(temp);
- }
-}
-
-void SettingsDialog::on_browseGameDirBtn_clicked()
-{
- QFileInfo oldGameExe(ui->managedGameDirEdit->text());
-
- QString temp = QFileDialog::getOpenFileName(this, tr("Select game executable"), oldGameExe.absolutePath(), oldGameExe.fileName());
- if (!temp.isEmpty()) {
- ui->managedGameDirEdit->setText(temp);
- }
-}
-
-void SettingsDialog::on_containsBtn_clicked()
-{
- QColor result = QColorDialog::getColor(m_ContainsColor, this, "Color Picker: Mod contains selected plugin", QColorDialog::ShowAlphaChannel);
- if (result.isValid()) {
- m_ContainsColor = result;
- setButtonColor(ui->containsBtn, result);
- }
-}
-
-void SettingsDialog::on_containedBtn_clicked()
-{
- QColor result = QColorDialog::getColor(m_ContainedColor, this, "ColorPicker: Plugin is Contained in selected Mod", QColorDialog::ShowAlphaChannel);
- if (result.isValid()) {
- m_ContainedColor = result;
- setButtonColor(ui->containedBtn, result);
- }
-}
-
-void SettingsDialog::on_overwrittenBtn_clicked()
-{
- QColor result = QColorDialog::getColor(m_OverwrittenColor, this, "ColorPicker: Is overwritten (loose files)", QColorDialog::ShowAlphaChannel);
- if (result.isValid()) {
- m_OverwrittenColor = result;
- setButtonColor(ui->overwrittenBtn, result);
- }
-}
-
-void SettingsDialog::on_overwritingBtn_clicked()
-{
- QColor result = QColorDialog::getColor(m_OverwritingColor, this, "ColorPicker: Is overwriting (loose files)", QColorDialog::ShowAlphaChannel);
- if (result.isValid()) {
- m_OverwritingColor = result;
- setButtonColor(ui->overwritingBtn, result);
- }
-}
-
-void SettingsDialog::on_overwrittenArchiveBtn_clicked()
-{
- QColor result = QColorDialog::getColor(m_OverwrittenArchiveColor, this, "ColorPicker: Is overwritten (archive files)", QColorDialog::ShowAlphaChannel);
- if (result.isValid()) {
- m_OverwrittenArchiveColor = result;
- setButtonColor(ui->overwrittenArchiveBtn, result);
- }
-}
-
-void SettingsDialog::on_overwritingArchiveBtn_clicked()
-{
- QColor result = QColorDialog::getColor(m_OverwritingArchiveColor, this, "ColorPicker: Is overwriting (archive files)", QColorDialog::ShowAlphaChannel);
- if (result.isValid()) {
- m_OverwritingArchiveColor = result;
- setButtonColor(ui->overwritingArchiveBtn, result);
- }
-}
-
-void SettingsDialog::on_resetColorsBtn_clicked()
-{
- m_OverwritingColor = QColor(255, 0, 0, 64);
- m_OverwrittenColor = QColor(0, 255, 0, 64);
- m_OverwritingArchiveColor = QColor(255, 0, 255, 64);
- m_OverwrittenArchiveColor = QColor(0, 255, 255, 64);
- m_ContainsColor = QColor(0, 0, 255, 64);
- m_ContainedColor = QColor(0, 0, 255, 64);
-
- setButtonColor(ui->overwritingBtn, m_OverwritingColor);
- setButtonColor(ui->overwrittenBtn, m_OverwrittenColor);
- setButtonColor(ui->overwritingArchiveBtn, m_OverwritingArchiveColor);
- setButtonColor(ui->overwrittenArchiveBtn, m_OverwrittenArchiveColor);
- setButtonColor(ui->containsBtn, m_ContainsColor);
- setButtonColor(ui->containedBtn, m_ContainedColor);
-}
-
-void SettingsDialog::on_resetDialogsButton_clicked()
-{
- if (QMessageBox::question(this, tr("Confirm?"),
- tr("This will make all dialogs show up again where you checked the \"Remember selection\"-box. Continue?"),
- QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) {
- emit resetDialogs();
- }
-}
-
-void SettingsDialog::on_nexusConnect_clicked()
-{
- fetchNexusApiKey();
-}
-
-void SettingsDialog::on_nexusManualKey_clicked()
-{
- NexusManualKeyDialog dialog(this);
-
- if (dialog.exec() != QDialog::Accepted) {
- return;
- }
-
- const auto key = dialog.key();
-
- if (key.isEmpty()) {
- clearKey();
- } else {
- if (setKey(key)) {
- testApiKey();
- }
- }
-}
-
-void SettingsDialog::fetchNexusApiKey()
-{
- QUrl url = QUrl("wss://sso.nexusmods.com");
- m_nexusLogin->open(url);
- updateNexusButtons();
-}
-
-void SettingsDialog::dispatchLogin()
-{
- m_KeyReceived = false;
- QJsonObject login;
- if (m_UUID.isEmpty()) {
- boost::uuids::random_generator generator;
- boost::uuids::uuid sessionId = generator();
- m_UUID = boost::uuids::to_string(sessionId).c_str();
- }
- login.insert(QString("id"), QJsonValue(m_UUID));
- login.insert(QString("token"), QJsonValue(m_AuthToken));
- login.insert(QString("protocol"), 2);
- QJsonDocument loginDoc(login);
- QString finalMessage(loginDoc.toJson());
- m_nexusLogin->sendTextMessage(finalMessage);
- QDesktopServices::openUrl(QUrl(QString("https://www.nexusmods.com/sso?id=%1&application=%2").arg(m_UUID).arg("modorganizer2")));
- m_loginTimer.start(30000);
-}
-void SettingsDialog::loginPing()
+SettingsTab::SettingsTab(Settings& s, SettingsDialog& d)
+ : ui(d.ui), m_settings(s), m_dialog(d)
{
- if (m_nexusLogin->isValid()) {
- m_nexusLogin->ping();
- m_totalPings++;
- }
- if (m_totalPings >= 60) {
- m_loginTimer.stop();
- m_totalPings = 0;
- m_nexusLogin->close(QWebSocketProtocol::CloseCodeGoingAway, "Timeout: No response received after thirty minutes. Cancelling request.");
- }
-}
-
-void SettingsDialog::authError(QAbstractSocket::SocketError error)
-{
- auto errorInfo = m_nexusLogin->errorString();
- qCritical() << "An error occurred: " << errorInfo;
-}
-
-void SettingsDialog::receiveApiKey(const QString &response)
-{
- QJsonDocument responseDoc = QJsonDocument::fromJson(response.toUtf8());
- QVariantMap responseData = responseDoc.object().toVariantMap();
- if (responseData["success"].toBool()) {
- QVariantMap data = responseData["data"].toMap();
- if (data.contains("connection_token")) {
- m_AuthToken = data["connection_token"].toString();
- } else {
- const auto key = data["api_key"].toString();
-
- m_nexusLogin->close();
- m_loginTimer.stop();
- m_totalPings = 0;
-
- if (key.isEmpty()) {
- clearKey();
- } else {
- setKey(key);
- }
- }
- } else {
- QString error("There was a problem with SSO initialization: %1");
- qCritical() << error.arg(responseData["error"].toString());
- m_nexusLogin->close();
- }
-}
-
-void SettingsDialog::completeApiConnection()
-{
- if (!m_KeyReceived && !m_loginTimer.isActive()) {
- QMessageBox::warning(qApp->activeWindow(), tr("Error"),
- tr("Failed to retrieve a Nexus API key! Please try again. "
- "A browser window should open asking you to authorize."));
-
- // try again
- fetchNexusApiKey();
- }
-}
-
-bool SettingsDialog::setKey(const QString& key)
-{
- m_KeyReceived = true;
- const bool ret = m_settings->setNexusApiKey(key);
- updateNexusButtons();
- return ret;
-}
-
-bool SettingsDialog::clearKey()
-{
- m_KeyCleared = true;
- const auto ret = m_settings->clearNexusApiKey();
- updateNexusButtons();
-
- NexusInterface::instance(m_PluginContainer)->getAccessManager()->clearApiKey();
-
- return ret;
}
-void SettingsDialog::testApiKey()
-{
- QString key;
- if (!m_settings->getNexusApiKey(key)) {
- qWarning().nospace() << "can't test API key, nothing stored";
- return;
- }
-
- NexusInterface::instance(m_PluginContainer)->getAccessManager()->apiCheck(key, true);
-}
-
-void SettingsDialog::updateNexusButtons()
-{
- if (m_nexusLogin->state() != QAbstractSocket::UnconnectedState) {
- // api key is in the process of being retrieved
- ui->nexusConnect->setText("Connecting the API. Please login within the browser and accept the request. This will time out after 30 minutes.");
- ui->nexusConnect->setEnabled(false);
- ui->nexusDisconnect->setEnabled(false);
- ui->nexusManualKey->setEnabled(false);
- }
- else if (m_settings->hasNexusApiKey()) {
- // api key is present
- ui->nexusConnect->setText("Nexus API Key Stored");
- ui->nexusConnect->setEnabled(false);
- ui->nexusDisconnect->setEnabled(true);
- ui->nexusManualKey->setEnabled(false);
- } else {
- // api key not present
- ui->nexusConnect->setText("Connect to Nexus");
- ui->nexusConnect->setEnabled(true);
- ui->nexusDisconnect->setEnabled(false);
- ui->nexusManualKey->setEnabled(true);
- }
-}
-
-void SettingsDialog::storeSettings(QListWidgetItem *pluginItem)
-{
- if (pluginItem != nullptr) {
- QVariantMap settings = pluginItem->data(Qt::UserRole + 1).toMap();
-
- for (int i = 0; i < ui->pluginSettingsList->topLevelItemCount(); ++i) {
- const QTreeWidgetItem *item = ui->pluginSettingsList->topLevelItem(i);
- settings[item->text(0)] = item->data(1, Qt::DisplayRole);
- }
-
- pluginItem->setData(Qt::UserRole + 1, settings);
- }
-}
-
-void SettingsDialog::on_pluginsList_currentItemChanged(QListWidgetItem *current, QListWidgetItem *previous)
-{
- storeSettings(previous);
-
- ui->pluginSettingsList->clear();
- IPlugin *plugin = static_cast<IPlugin*>(current->data(Qt::UserRole).value<void*>());
- ui->authorLabel->setText(plugin->author());
- ui->versionLabel->setText(plugin->version().canonicalString());
- ui->descriptionLabel->setText(plugin->description());
-
- QVariantMap settings = current->data(Qt::UserRole + 1).toMap();
- QVariantMap descriptions = current->data(Qt::UserRole + 2).toMap();
- ui->pluginSettingsList->setEnabled(settings.count() != 0);
- for (auto iter = settings.begin(); iter != settings.end(); ++iter) {
- QTreeWidgetItem *newItem = new QTreeWidgetItem(QStringList(iter.key()));
- QVariant value = *iter;
- QString description;
- {
- auto descriptionIter = descriptions.find(iter.key());
- if (descriptionIter != descriptions.end()) {
- description = descriptionIter->toString();
- }
- }
-
- ui->pluginSettingsList->setItemDelegateForColumn(0, new NoEditDelegate());
- newItem->setData(1, Qt::DisplayRole, value);
- newItem->setData(1, Qt::EditRole, value);
- newItem->setToolTip(1, description);
-
- newItem->setFlags(newItem->flags() | Qt::ItemIsEditable);
- ui->pluginSettingsList->addTopLevelItem(newItem);
- }
-
- ui->pluginSettingsList->resizeColumnToContents(0);
- ui->pluginSettingsList->resizeColumnToContents(1);
-}
-
-void SettingsDialog::deleteBlacklistItem()
-{
- ui->pluginBlacklist->takeItem(ui->pluginBlacklist->currentIndex().row());
-}
-
-void SettingsDialog::on_associateButton_clicked()
-{
- Settings::instance().registerAsNXMHandler(true);
-}
-
-void SettingsDialog::on_clearCacheButton_clicked()
-{
- QDir(Settings::instance().getCacheDirectory()).removeRecursively();
- NexusInterface::instance(m_PluginContainer)->clearCache();
-}
-
-void SettingsDialog::on_nexusDisconnect_clicked()
-{
- clearKey();
-}
-
-void SettingsDialog::normalizePath(QLineEdit *lineEdit)
-{
- QString text = lineEdit->text();
- while (text.endsWith('/') || text.endsWith('\\')) {
- text.chop(1);
- }
- lineEdit->setText(text);
-}
-
-void SettingsDialog::on_baseDirEdit_editingFinished()
-{
- normalizePath(ui->baseDirEdit);
-}
-
-void SettingsDialog::on_downloadDirEdit_editingFinished()
-{
- normalizePath(ui->downloadDirEdit);
-}
-
-void SettingsDialog::on_modDirEdit_editingFinished()
-{
- normalizePath(ui->modDirEdit);
-}
-
-void SettingsDialog::on_cacheDirEdit_editingFinished()
-{
- normalizePath(ui->cacheDirEdit);
-}
+SettingsTab::~SettingsTab() = default;
-void SettingsDialog::on_profilesDirEdit_editingFinished()
+Settings& SettingsTab::settings()
{
- normalizePath(ui->profilesDirEdit);
+ return m_settings;
}
-void SettingsDialog::on_overwriteDirEdit_editingFinished()
+SettingsDialog& SettingsTab::dialog()
{
- normalizePath(ui->overwriteDirEdit);
+ return m_dialog;
}
-void SettingsDialog::on_resetGeometryBtn_clicked()
+QWidget* SettingsTab::parentWidget()
{
- m_GeometriesReset = true;
- ui->resetGeometryBtn->setChecked(true);
+ return m_dialog.parentWidgetForDialogs();
}
diff --git a/src/settingsdialog.h b/src/settingsdialog.h
index 858a36d4..bae4a469 100644
--- a/src/settingsdialog.h
+++ b/src/settingsdialog.h
@@ -21,19 +21,35 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#define SETTINGSDIALOG_H
#include "tutorabledialog.h"
-#include <iplugin.h>
-#include <QComboBox>
-#include <QDialog>
-#include <QWebSocket>
-#include <QListWidgetItem>
-#include <QTimer>
+#include "util.h"
class PluginContainer;
class Settings;
+class SettingsDialog;
+namespace Ui { class SettingsDialog; }
+
+
+class SettingsTab
+{
+public:
+ SettingsTab(Settings& settings, SettingsDialog& m_dialog);
+ virtual ~SettingsTab();
+
+ virtual void update() = 0;
+ virtual void closing() {}
+
+protected:
+ Ui::SettingsDialog* ui;
+
+ Settings& settings();
+ SettingsDialog& dialog();
+ QWidget* parentWidget();
+
+private:
+ Settings& m_settings;
+ SettingsDialog& m_dialog;
+};
-namespace Ui {
- class SettingsDialog;
-}
/**
* dialog used to change settings for Mod Organizer. On top of the
@@ -42,11 +58,12 @@ namespace Ui {
**/
class SettingsDialog : public MOBase::TutorableDialog
{
- Q_OBJECT
+ Q_OBJECT;
+ friend class SettingsTab;
public:
explicit SettingsDialog(
- PluginContainer *pluginContainer, Settings* settings, QWidget *parent = 0);
+ PluginContainer* pluginContainer, Settings& settings, QWidget* parent = 0);
~SettingsDialog();
@@ -56,112 +73,23 @@ public:
*/
QString getColoredButtonStyleSheet() const;
- void setButtonColor(QPushButton *button, const QColor &color);
-
-public slots:
-
- virtual void accept();
-
-signals:
+ PluginContainer* pluginContainer();
+ QWidget* parentWidgetForDialogs();
- void resetDialogs();
- void retryApiConnection();
+ void setExitNeeded(ExitFlags e);
+ ExitFlags exitNeeded() const;
-private:
-
- void storeSettings(QListWidgetItem *pluginItem);
- void normalizePath(QLineEdit *lineEdit);
-
-public:
-
- QColor getOverwritingColor() { return m_OverwritingColor; }
- QColor getOverwrittenColor() { return m_OverwrittenColor; }
- QColor getOverwritingArchiveColor() { return m_OverwritingArchiveColor; }
- QColor getOverwrittenArchiveColor() { return m_OverwrittenArchiveColor; }
- QColor getContainsColor() { return m_ContainsColor; }
- QColor getContainedColor() { return m_ContainedColor; }
- QString getExecutableBlacklist() { return m_ExecutableBlacklist; }
- bool getResetGeometries();
- bool getApiKeyChanged();
-
- void setOverwritingColor(QColor col) { m_OverwritingColor = col; }
- void setOverwrittenColor(QColor col) { m_OverwrittenColor = col; }
- void setOverwritingArchiveColor(QColor col) { m_OverwritingArchiveColor = col; }
- void setOverwrittenArchiveColor(QColor col) { m_OverwrittenArchiveColor = col; }
- void setContainsColor(QColor col) { m_ContainsColor = col; }
- void setContainedColor(QColor col) { m_ContainedColor = col; }
- void setExecutableBlacklist(QString blacklist) { m_ExecutableBlacklist = blacklist; }
-
-
-private slots:
- void on_categoriesBtn_clicked();
- void on_execBlacklistBtn_clicked();
- void on_bsaDateBtn_clicked();
- void on_browseDownloadDirBtn_clicked();
- void on_browseModDirBtn_clicked();
- void on_browseCacheDirBtn_clicked();
- void on_resetDialogsButton_clicked();
- void on_pluginsList_currentItemChanged(QListWidgetItem *current, QListWidgetItem *previous);
- void on_associateButton_clicked();
- void on_clearCacheButton_clicked();
- void on_nexusDisconnect_clicked();
- void on_browseBaseDirBtn_clicked();
- void on_browseOverwriteDirBtn_clicked();
- void on_browseProfilesDirBtn_clicked();
- void on_browseGameDirBtn_clicked();
- void on_overwritingBtn_clicked();
- void on_overwrittenBtn_clicked();
- void on_overwritingArchiveBtn_clicked();
- void on_overwrittenArchiveBtn_clicked();
- void on_containsBtn_clicked();
- void on_containedBtn_clicked();
- void on_resetColorsBtn_clicked();
- void on_baseDirEdit_editingFinished();
- void on_downloadDirEdit_editingFinished();
- void on_modDirEdit_editingFinished();
- void on_cacheDirEdit_editingFinished();
- void on_profilesDirEdit_editingFinished();
- void on_overwriteDirEdit_editingFinished();
- void on_nexusConnect_clicked();
- void on_nexusManualKey_clicked();
- void on_resetGeometryBtn_clicked();
+ int exec() override;
- void deleteBlacklistItem();
- void dispatchLogin();
- void loginPing();
- void authError(QAbstractSocket::SocketError error);
- void receiveApiKey(const QString &apiKey);
- void completeApiConnection();
+public slots:
+ virtual void accept();
private:
- Ui::SettingsDialog *ui;
- Settings* m_settings;
- PluginContainer *m_PluginContainer;
-
- QColor m_OverwritingColor;
- QColor m_OverwrittenColor;
- QColor m_OverwritingArchiveColor;
- QColor m_OverwrittenArchiveColor;
- QColor m_ContainsColor;
- QColor m_ContainedColor;
-
- bool m_KeyReceived;
- bool m_KeyCleared;
- bool m_GeometriesReset;
- QString m_UUID;
- QString m_AuthToken;
-
- QString m_ExecutableBlacklist;
- QWebSocket *m_nexusLogin;
- QTimer m_loginTimer;
- int m_totalPings = 0;
-
- bool setKey(const QString& key);
- bool clearKey();
- void updateNexusButtons();
-
- void fetchNexusApiKey();
- void testApiKey();
+ Ui::SettingsDialog* ui;
+ Settings& m_settings;
+ std::vector<std::unique_ptr<SettingsTab>> m_tabs;
+ ExitFlags m_exit;
+ PluginContainer* m_pluginContainer;
};
#endif // SETTINGSDIALOG_H
diff --git a/src/settingsdialog.ui b/src/settingsdialog.ui
index faaf1653..2ceb91db 100644
--- a/src/settingsdialog.ui
+++ b/src/settingsdialog.ui
@@ -7,7 +7,7 @@
<x>0</x>
<y>0</y>
<width>586</width>
- <height>486</height>
+ <height>491</height>
</rect>
</property>
<property name="windowTitle">
@@ -23,164 +23,129 @@
<attribute name="title">
<string>General</string>
</attribute>
- <layout class="QVBoxLayout" name="verticalLayout_3">
- <item>
- <layout class="QHBoxLayout" name="horizontalLayout_3">
- <item>
- <widget class="QLabel" name="label_5">
- <property name="text">
- <string>Language</string>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QComboBox" name="languageBox">
- <property name="toolTip">
- <string>The display language</string>
- </property>
- <property name="whatsThis">
- <string>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;
-&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;
-p, li { white-space: pre-wrap; }
-&lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:'MS Shell Dlg 2'; font-size:8.25pt; font-weight:400; font-style:normal;&quot;&gt;
-&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-size:8pt;&quot;&gt;The display language. This will only displaye languages for which you have a translation installed.&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
- </property>
- </widget>
- </item>
- </layout>
- </item>
- <item>
- <layout class="QHBoxLayout" name="horizontalLayout_4">
- <item>
- <widget class="QLabel" name="label_10">
- <property name="text">
- <string>Style</string>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QComboBox" name="styleBox">
- <property name="toolTip">
- <string>graphical style</string>
- </property>
- <property name="whatsThis">
- <string>graphical style of the MO user interface</string>
- </property>
- </widget>
- </item>
- </layout>
- </item>
- <item>
- <widget class="QCheckBox" name="usePrereleaseBox">
- <property name="toolTip">
- <string>Update to non-stable releases.</string>
- </property>
- <property name="whatsThis">
- <string>If this is enabled, the integrated update mechanism will notify of all releases, including pre-releases (alphas, betas).
-
-Please use this only if you're sufficiently tech-savvy to investigate issues, look for known problems in the issue tracker and create meaningful reports.
-
-If you use pre-releases, never contact me directly by e-mail or via private messages!</string>
+ <layout class="QGridLayout" name="gridLayout" rowstretch="0,0,0,0,0,1">
+ <item row="5" column="0" colspan="2">
+ <spacer name="verticalSpacer">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
</property>
- <property name="text">
- <string>Install Pre-releases (Betas)</string>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>0</width>
+ <height>0</height>
+ </size>
</property>
- </widget>
+ </spacer>
</item>
- <item>
+ <item row="0" column="0" rowspan="2">
<widget class="QGroupBox" name="groupBox">
<property name="title">
- <string>User interface</string>
+ <string>User Interface</string>
</property>
- <layout class="QGridLayout" name="gridLayout">
- <item row="2" column="0" colspan="2">
- <widget class="QGroupBox" name="ModlistGroupBox">
- <property name="title">
- <string>Colors</string>
- </property>
- <layout class="QGridLayout" name="gridLayout_5">
- <item row="6" column="0">
- <widget class="QCheckBox" name="colorSeparatorsBox">
- <property name="toolTip">
- <string>When this is enabled, the color defined for a separator will be shown on the mod list scrollbar at the location of the separator. This can be useful for quick navigation between separator sections or to a specific separator section.</string>
- </property>
- <property name="whatsThis">
- <string>When this is enabled, the color defined for a separator will be shown on the mod list scrollbar at the location of the separator. This can be useful for quick navigation between separator sections or to a specific separator section.</string>
- </property>
+ <layout class="QVBoxLayout" name="verticalLayout_14">
+ <item>
+ <widget class="QWidget" name="widget_6" native="true">
+ <layout class="QGridLayout" name="gridLayout_5" columnstretch="0,1,0">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item row="0" column="0">
+ <widget class="QLabel" name="label_5">
<property name="text">
- <string>Show mod list separator colors on the scrollbar</string>
- </property>
- <property name="checked">
- <bool>true</bool>
+ <string>Language</string>
</property>
</widget>
</item>
- <item row="4" column="0" colspan="2">
- <widget class="QPushButton" name="containedBtn">
+ <item row="2" column="0">
+ <widget class="QLabel" name="label_10">
<property name="text">
- <string>Plugin is Contained in selected Mod</string>
+ <string>Style</string>
</property>
</widget>
</item>
- <item row="1" column="0">
- <widget class="QPushButton" name="overwrittenBtn">
- <property name="text">
- <string>Is overwritten (loose files)</string>
+ <item row="2" column="1">
+ <widget class="QComboBox" name="styleBox">
+ <property name="toolTip">
+ <string>Visual theme of the user interface.</string>
</property>
- </widget>
- </item>
- <item row="1" column="1">
- <widget class="QPushButton" name="overwritingBtn">
- <property name="text">
- <string>Is overwriting (loose files)</string>
+ <property name="whatsThis">
+ <string>Visual theme of the user interface.</string>
</property>
</widget>
</item>
- <item row="5" column="0" colspan="2">
- <widget class="QPushButton" name="resetColorsBtn">
+ <item row="2" column="2">
+ <widget class="QPushButton" name="exploreStyles">
<property name="text">
- <string>Reset Colors</string>
+ <string>Explore...</string>
</property>
</widget>
</item>
- <item row="3" column="0" colspan="2">
- <widget class="QPushButton" name="containsBtn">
- <property name="text">
- <string>Mod Contains selected Plugin</string>
+ <item row="0" column="1" colspan="2">
+ <widget class="QComboBox" name="languageBox">
+ <property name="toolTip">
+ <string>The language of the user interface.</string>
</property>
- </widget>
- </item>
- <item row="2" column="0">
- <widget class="QPushButton" name="overwrittenArchiveBtn">
- <property name="text">
- <string>Is overwritten (archive files)</string>
+ <property name="whatsThis">
+ <string>The language of the user interface.</string>
</property>
</widget>
</item>
- <item row="2" column="1">
- <widget class="QPushButton" name="overwritingArchiveBtn">
+ <item row="1" column="1" colspan="2">
+ <widget class="LinkLabel" name="label_33">
+ <property name="toolTip">
+ <string>https://www.transifex.com/mod-organizer-2-team/mod-organizer-2/</string>
+ </property>
<property name="text">
- <string>Is overwriting (archive files)</string>
+ <string>&lt;a href=&quot;https://www.transifex.com/mod-organizer-2-team/mod-organizer-2/&quot;&gt;Help translate Mod Organizer&lt;/a&gt;</string>
+ </property>
+ <property name="openExternalLinks">
+ <bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
- <item row="6" column="0" colspan="2">
- <widget class="QPushButton" name="categoriesBtn">
+ <item>
+ <widget class="QCheckBox" name="centerDialogs">
<property name="toolTip">
- <string>Modify the categories available to arrange your mods.</string>
+ <string>Dialogs will always be centered on the main window, but will remember their size.</string>
</property>
<property name="whatsThis">
- <string>Modify the categories available to arrange your mods.</string>
+ <string>Dialogs will always be centered on the main window, but will remember their size.</string>
</property>
<property name="text">
- <string>Configure Mod Categories</string>
+ <string>Always center dialogs</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="changeGameConfirmation">
+ <property name="text">
+ <string>Show confirmation when changing instance</string>
</property>
</widget>
</item>
- <item row="5" column="0" colspan="2">
+ <item>
+ <widget class="QCheckBox" name="doubleClickPreviews">
+ <property name="toolTip">
+ <string>Whether double-clicking on a file opens the preview window or launches the program associated with it. This applies to the Data tab as well as the Conflicts and Filetree tabs in the mod info window.</string>
+ </property>
+ <property name="text">
+ <string>Open previews on double-click</string>
+ </property>
+ </widget>
+ </item>
+ <item>
<widget class="QPushButton" name="resetDialogsButton">
<property name="maximumSize">
<size>
@@ -189,49 +154,238 @@ If you use pre-releases, never contact me directly by e-mail or via private mess
</size>
</property>
<property name="toolTip">
- <string>Reset stored information from dialogs.</string>
+ <string>Reset all choices made in dialogs.</string>
</property>
<property name="whatsThis">
- <string>This will make all dialogs show up again where you checked the &quot;Remember selection&quot;-box.</string>
+ <string>Reset all choices made in dialogs.</string>
</property>
<property name="text">
- <string>Reset Dialogs</string>
+ <string>Reset Dialog Choices</string>
</property>
</widget>
</item>
- <item row="0" column="0">
- <widget class="QCheckBox" name="compactBox">
+ <item>
+ <widget class="QPushButton" name="categoriesBtn">
<property name="toolTip">
- <string>If checked, the download interface will be more compact.</string>
+ <string>Modify the categories available to arrange your mods.</string>
+ </property>
+ <property name="whatsThis">
+ <string>Modify the categories available to arrange your mods.</string>
</property>
<property name="text">
- <string>Compact Download Interface</string>
+ <string>Configure Mod Categories</string>
</property>
</widget>
</item>
- <item row="4" column="0" colspan="2">
- <spacer name="verticalSpacer">
+ <item>
+ <spacer name="verticalSpacer_11">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
- <width>20</width>
- <height>40</height>
+ <width>0</width>
+ <height>0</height>
</size>
</property>
</spacer>
</item>
- <item row="0" column="1">
+ </layout>
+ </widget>
+ </item>
+ <item row="0" column="1">
+ <widget class="QGroupBox" name="groupBox_5">
+ <property name="title">
+ <string>Download List</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_2">
+ <item>
<widget class="QCheckBox" name="showMetaBox">
<property name="toolTip">
- <string>If checked, the download list will display meta information instead of file names.</string>
+ <string>Show meta information instead of file names in the download list.</string>
+ </property>
+ <property name="whatsThis">
+ <string>Show meta information instead of file names in the download list.</string>
+ </property>
+ <property name="text">
+ <string>Show Meta Information</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="compactBox">
+ <property name="toolTip">
+ <string>Make the download list more compact.</string>
+ </property>
+ <property name="whatsThis">
+ <string>Make the download list more compact.</string>
</property>
<property name="text">
- <string>Download Meta Information</string>
+ <string>Compact List</string>
</property>
</widget>
</item>
+ <item>
+ <spacer name="verticalSpacer_5">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>0</width>
+ <height>0</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item row="3" column="0" colspan="2">
+ <widget class="QGroupBox" name="ModlistGroupBox">
+ <property name="title">
+ <string>Colors</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_13">
+ <item>
+ <widget class="ColorTable" name="colorTable">
+ <property name="editTriggers">
+ <set>QAbstractItemView::NoEditTriggers</set>
+ </property>
+ <property name="selectionMode">
+ <enum>QAbstractItemView::SingleSelection</enum>
+ </property>
+ <property name="selectionBehavior">
+ <enum>QAbstractItemView::SelectRows</enum>
+ </property>
+ <property name="verticalScrollMode">
+ <enum>QAbstractItemView::ScrollPerPixel</enum>
+ </property>
+ <property name="cornerButtonEnabled">
+ <bool>false</bool>
+ </property>
+ <attribute name="horizontalHeaderVisible">
+ <bool>false</bool>
+ </attribute>
+ <attribute name="horizontalHeaderHighlightSections">
+ <bool>false</bool>
+ </attribute>
+ <attribute name="horizontalHeaderStretchLastSection">
+ <bool>true</bool>
+ </attribute>
+ <attribute name="verticalHeaderVisible">
+ <bool>false</bool>
+ </attribute>
+ </widget>
+ </item>
+ <item>
+ <widget class="QWidget" name="widget_5" native="true">
+ <layout class="QHBoxLayout" name="horizontalLayout_3">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QCheckBox" name="colorSeparatorsBox">
+ <property name="toolTip">
+ <string>Colors set on separators will also be shown in the mod list scrollbar at the location of the separator. This can be useful for quickly navigating to a specific separator.</string>
+ </property>
+ <property name="whatsThis">
+ <string>Colors set on separators will also be shown in the mod list scrollbar at the location of the separator. This can be useful for quickly navigating to a specific separator.</string>
+ </property>
+ <property name="text">
+ <string>Show mod list separator colors on the scrollbar</string>
+ </property>
+ <property name="checked">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer_2">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>40</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QPushButton" name="resetColorsBtn">
+ <property name="toolTip">
+ <string>Reset all colors to their default value.</string>
+ </property>
+ <property name="whatsThis">
+ <string>Reset all colors to their default value.</string>
+ </property>
+ <property name="text">
+ <string>Reset Colors</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item row="1" column="1">
+ <widget class="QGroupBox" name="groupBox_6">
+ <property name="title">
+ <string>Updates</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_3">
+ <item>
+ <widget class="QCheckBox" name="checkForUpdates">
+ <property name="toolTip">
+ <string>Check for Mod Organizer updates on Github on startup.</string>
+ </property>
+ <property name="whatsThis">
+ <string>Check for Mod Organizer updates on Github on startup.</string>
+ </property>
+ <property name="text">
+ <string>Check for updates</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="usePrereleaseBox">
+ <property name="toolTip">
+ <string>Update to non-stable releases.</string>
+ </property>
+ <property name="whatsThis">
+ <string>Update to non-stable releases.</string>
+ </property>
+ <property name="text">
+ <string>Install Pre-releases (Betas)</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="verticalSpacer_9">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>0</width>
+ <height>0</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
</layout>
</widget>
</item>
@@ -451,112 +605,237 @@ If you use pre-releases, never contact me directly by e-mail or via private mess
<attribute name="title">
<string>Nexus</string>
</attribute>
- <layout class="QVBoxLayout" name="verticalLayout_4">
+ <layout class="QVBoxLayout" name="verticalLayout_4" stretch="0,1,0,0,0,1">
+ <item>
+ <widget class="QWidget" name="widget" native="true">
+ <layout class="QHBoxLayout" name="horizontalLayout_5">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QGroupBox" name="groupBox_2">
+ <property name="title">
+ <string>Nexus Account</string>
+ </property>
+ <layout class="QFormLayout" name="formLayout">
+ <property name="horizontalSpacing">
+ <number>10</number>
+ </property>
+ <item row="0" column="0">
+ <widget class="QLabel" name="label_2">
+ <property name="text">
+ <string>User ID:</string>
+ </property>
+ <property name="textInteractionFlags">
+ <set>Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1">
+ <widget class="QLabel" name="nexusUserID">
+ <property name="text">
+ <string>id</string>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="0">
+ <widget class="QLabel" name="label_3">
+ <property name="text">
+ <string>Name:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="1">
+ <widget class="QLabel" name="nexusName">
+ <property name="text">
+ <string>name</string>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="0">
+ <widget class="QLabel" name="label_11">
+ <property name="text">
+ <string>Account:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="1">
+ <widget class="QLabel" name="nexusAccount">
+ <property name="text">
+ <string>account</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QGroupBox" name="groupBox_3">
+ <property name="title">
+ <string>Statistics</string>
+ </property>
+ <layout class="QFormLayout" name="formLayout_3">
+ <property name="horizontalSpacing">
+ <number>10</number>
+ </property>
+ <item row="0" column="0">
+ <widget class="QLabel" name="label_30">
+ <property name="text">
+ <string>Daily requests:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1">
+ <widget class="QLabel" name="nexusDailyRequests">
+ <property name="text">
+ <string>daily requests</string>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="0">
+ <widget class="QLabel" name="label_31">
+ <property name="text">
+ <string>Hourly requests:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="1">
+ <widget class="QLabel" name="nexusHourlyRequests">
+ <property name="text">
+ <string>hourly requests</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
<item>
<widget class="QGroupBox" name="nexusBox">
- <property name="toolTip">
- <string>Allows Mod Organizer to connect to the Nexus for downloading mods, checking for updates, and other such things.</string>
- </property>
- <property name="whatsThis">
- <string>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;
-&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;
-p, li { white-space: pre-wrap; }
-&lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:'MS Shell Dlg 2'; font-size:8pt; font-weight:400; font-style:normal;&quot;&gt;
-&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;Allows Mod Organizer to connect to the Nexus for downloading mods, checking for updates, and other such things. Clicking &amp;quot;Connect to Nexus&amp;quot; will open a Nexus webpage to authorise Mod Organizer. You will need to be logged into your Nexus account. The authorisation is stored in the Windows Credential Manager. Your Nexus username and password are not required or stored by Mod Organizer.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
- </property>
<property name="title">
- <string>Nexus</string>
+ <string>Nexus Connection</string>
</property>
- <layout class="QVBoxLayout" name="verticalLayout_2">
+ <layout class="QHBoxLayout" name="horizontalLayout_14">
<item>
- <layout class="QHBoxLayout" name="horizontalLayout_10">
- <item>
- <widget class="QPushButton" name="nexusConnect">
- <property name="text">
- <string>Connect to Nexus</string>
- </property>
- </widget>
- </item>
- </layout>
+ <widget class="QWidget" name="widget_2" native="true">
+ <layout class="QVBoxLayout" name="verticalLayout_11">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QPushButton" name="nexusConnect">
+ <property name="text">
+ <string>Connect to Nexus</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="nexusManualKey">
+ <property name="toolTip">
+ <string>Manually enter the API key and try to login</string>
+ </property>
+ <property name="text">
+ <string>Enter API Key Manually</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="nexusDisconnect">
+ <property name="toolTip">
+ <string>Clear the stored Nexus API key and force reauthorization.</string>
+ </property>
+ <property name="text">
+ <string>Disconnect from Nexus</string>
+ </property>
+ <property name="icon">
+ <iconset resource="resources.qrc">
+ <normaloff>:/MO/gui/edit_clear</normaloff>:/MO/gui/edit_clear</iconset>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="verticalSpacer_3">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>0</width>
+ <height>0</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </widget>
</item>
<item>
- <layout class="QHBoxLayout" name="horizontalLayout_11">
- <item>
- <spacer name="horizontalSpacer_7">
- <property name="orientation">
- <enum>Qt::Horizontal</enum>
- </property>
- <property name="sizeHint" stdset="0">
- <size>
- <width>40</width>
- <height>20</height>
- </size>
- </property>
- </spacer>
- </item>
- <item>
- <widget class="QPushButton" name="nexusManualKey">
- <property name="sizePolicy">
- <sizepolicy hsizetype="Minimum" vsizetype="Preferred">
- <horstretch>0</horstretch>
- <verstretch>0</verstretch>
- </sizepolicy>
- </property>
- <property name="toolTip">
- <string>Manually enter the API key and try to login</string>
- </property>
- <property name="text">
- <string>Enter API Key Manually</string>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QPushButton" name="nexusDisconnect">
- <property name="toolTip">
- <string>Clear the stored Nexus API key and force reauthorization.</string>
- </property>
- <property name="text">
- <string>Disconnect from Nexus</string>
- </property>
- <property name="icon">
- <iconset resource="resources.qrc">
- <normaloff>:/MO/gui/edit_clear</normaloff>:/MO/gui/edit_clear</iconset>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QPushButton" name="clearCacheButton">
- <property name="toolTip">
- <string>Remove cache and cookies.</string>
- </property>
- <property name="text">
- <string>Clear Cache</string>
- </property>
- <property name="icon">
- <iconset resource="resources.qrc">
- <normaloff>:/MO/gui/edit_clear</normaloff>:/MO/gui/edit_clear</iconset>
- </property>
- </widget>
- </item>
- <item>
- <spacer name="horizontalSpacer_5">
- <property name="orientation">
- <enum>Qt::Horizontal</enum>
- </property>
- <property name="sizeHint" stdset="0">
- <size>
- <width>40</width>
- <height>20</height>
- </size>
- </property>
- </spacer>
- </item>
- </layout>
+ <widget class="QWidget" name="widget_3" native="true">
+ <layout class="QVBoxLayout" name="verticalLayout_12">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QListWidget" name="nexusLog">
+ <property name="sizeAdjustPolicy">
+ <enum>QAbstractScrollArea::AdjustToContents</enum>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
</item>
</layout>
</widget>
</item>
<item>
+ <widget class="QWidget" name="widget_4" native="true">
+ <layout class="QHBoxLayout" name="horizontalLayout_10">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ </layout>
+ </widget>
+ </item>
+ <item>
<layout class="QGridLayout" name="gridLayout_8">
<item row="0" column="0">
<widget class="QCheckBox" name="offlineBox">
@@ -597,10 +876,10 @@ p, li { white-space: pre-wrap; }
<item row="1" column="1">
<widget class="QCheckBox" name="hideAPICounterBox">
<property name="toolTip">
- <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;By default, a counter is displayed under the mod list. This informs the user of their remaining API requests. The Nexus API becomes unusable once these API requests run out. Checking this option will hide that counter.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+ <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;By default, a counter is displayed in the bottom right corner. This informs the user of their remaining API requests. The Nexus API becomes unusable once these API requests run out. Checking this option will hide that counter.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="whatsThis">
- <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;By default, a counter is displayed under the mod list. This informs the user of their remaining API requests. The Nexus API becomes unusable once these API requests run out. Checking this option will hide that counter.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+ <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;By default, a counter is displayed in the bottom right corner. This informs the user of their remaining API requests. The Nexus API becomes unusable once these API requests run out. Checking this option will hide that counter.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Hide API Request Counter</string>
@@ -619,6 +898,20 @@ p, li { white-space: pre-wrap; }
</widget>
</item>
<item>
+ <widget class="QPushButton" name="clearCacheButton">
+ <property name="toolTip">
+ <string>Remove cache and cookies.</string>
+ </property>
+ <property name="text">
+ <string>Clear Cache</string>
+ </property>
+ <property name="icon">
+ <iconset resource="resources.qrc">
+ <normaloff>:/MO/gui/edit_clear</normaloff>:/MO/gui/edit_clear</iconset>
+ </property>
+ </widget>
+ </item>
+ <item>
<spacer name="horizontalSpacer_4">
<property name="orientation">
<enum>Qt::Horizontal</enum>
@@ -682,19 +975,6 @@ p, li { white-space: pre-wrap; }
</item>
</layout>
</item>
- <item>
- <spacer name="verticalSpacer_3">
- <property name="orientation">
- <enum>Qt::Vertical</enum>
- </property>
- <property name="sizeHint" stdset="0">
- <size>
- <width>20</width>
- <height>40</height>
- </size>
- </property>
- </spacer>
- </item>
</layout>
</widget>
<widget class="QWidget" name="steamTab">
@@ -702,71 +982,55 @@ p, li { white-space: pre-wrap; }
<string>Steam</string>
</attribute>
<layout class="QGridLayout" name="gridLayout_3">
- <item row="0" column="0">
+ <item row="1" column="0">
<widget class="QLabel" name="label_19">
<property name="text">
<string>Username</string>
</property>
</widget>
</item>
- <item row="0" column="1">
- <widget class="QLineEdit" name="steamUserEdit"/>
- </item>
- <item row="1" column="0">
- <widget class="QLabel" name="label_20">
- <property name="text">
- <string>Password</string>
- </property>
- </widget>
- </item>
- <item row="1" column="1">
- <widget class="QLineEdit" name="steamPassEdit">
- <property name="echoMode">
- <enum>QLineEdit::Password</enum>
- </property>
- </widget>
- </item>
- <item row="2" column="0">
- <spacer name="verticalSpacer_5">
+ <item row="4" column="0">
+ <spacer name="verticalSpacer_4">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeType">
- <enum>QSizePolicy::Minimum</enum>
+ <enum>QSizePolicy::Expanding</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
- <height>40</height>
+ <height>232</height>
</size>
</property>
</spacer>
</item>
- <item row="3" column="0" colspan="2">
+ <item row="1" column="1">
+ <widget class="QLineEdit" name="steamUserEdit"/>
+ </item>
+ <item row="0" column="0" colspan="2">
<widget class="QLabel" name="label_21">
<property name="text">
- <string>If you save your steam user ID and password here, they will be used when logging into steam. Note, however, your password will be stored unencrypted, so make sure your computer is secure.</string>
+ <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;If you save your steam user ID and password here, they will be used when logging into steam.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
- <item row="4" column="0">
- <spacer name="verticalSpacer_4">
- <property name="orientation">
- <enum>Qt::Vertical</enum>
- </property>
- <property name="sizeType">
- <enum>QSizePolicy::Expanding</enum>
+ <item row="2" column="0">
+ <widget class="QLabel" name="label_20">
+ <property name="text">
+ <string>Password</string>
</property>
- <property name="sizeHint" stdset="0">
- <size>
- <width>20</width>
- <height>232</height>
- </size>
+ </widget>
+ </item>
+ <item row="2" column="1">
+ <widget class="QLineEdit" name="steamPassEdit">
+ <property name="echoMode">
+ <enum>QLineEdit::Password</enum>
</property>
- </spacer>
+ </widget>
</item>
</layout>
</widget>
@@ -1127,9 +1391,6 @@ programs you are intentionally running.</string>
<property name="text">
<string>Reset Window Geometries</string>
</property>
- <property name="checkable">
- <bool>true</bool>
- </property>
</widget>
</item>
<item>
@@ -1162,45 +1423,24 @@ programs you are intentionally running.</string>
<string>Diagnostics</string>
</attribute>
<layout class="QGridLayout" name="gridLayout_4">
- <item row="2" column="0">
- <layout class="QHBoxLayout" name="horizontalLayout_13">
- <item>
- <widget class="QLabel" name="label_28">
- <property name="text">
- <string>Max Dumps To Keep</string>
- </property>
- </widget>
- </item>
- <item>
- <spacer name="horizontalSpacer_6">
- <property name="orientation">
- <enum>Qt::Horizontal</enum>
- </property>
- <property name="sizeHint" stdset="0">
- <size>
- <width>60</width>
- <height>20</height>
- </size>
- </property>
- </spacer>
- </item>
- <item>
- <widget class="QSpinBox" name="dumpsMaxEdit">
- <property name="toolTip">
- <string>Maximum number of crash dumps to keep on disk. Use 0 for unlimited.</string>
- </property>
- <property name="whatsThis">
- <string>
- Maximum number of crash dumps to keep on disk. Use 0 for unlimited.
- Set &quot;Crash Dumps&quot; above to None to disable crash dump collection.
- </string>
- </property>
- </widget>
- </item>
- </layout>
- </item>
<item row="3" column="0">
- <widget class="QLabel" name="diagnosticsExplainedLabel">
+ <spacer name="verticalSpacer_10">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeType">
+ <enum>QSizePolicy::Expanding</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>232</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item row="2" column="0">
+ <widget class="LinkLabel" name="diagnosticsExplainedLabel">
<property name="toolTip">
<string>Hint: right click link and copy link location</string>
</property>
@@ -1215,18 +1455,47 @@ programs you are intentionally running.</string>
<property name="wordWrap">
<bool>true</bool>
</property>
+ <property name="openExternalLinks">
+ <bool>true</bool>
+ </property>
</widget>
</item>
- <item row="1" column="0">
- <layout class="QHBoxLayout" name="horizontalLayout_12">
- <item>
+ <item row="0" column="0">
+ <layout class="QFormLayout" name="formLayout_5">
+ <property name="fieldGrowthPolicy">
+ <enum>QFormLayout::ExpandingFieldsGrow</enum>
+ </property>
+ <property name="horizontalSpacing">
+ <number>12</number>
+ </property>
+ <item row="0" column="0">
+ <widget class="QLabel" name="label_12">
+ <property name="text">
+ <string>Log Level</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1">
+ <widget class="QComboBox" name="logLevelBox">
+ <property name="toolTip">
+ <string>Decides the amount of data printed to &quot;ModOrganizer.log&quot;</string>
+ </property>
+ <property name="whatsThis">
+ <string>
+ Decides the amount of data printed to &quot;ModOrganizer.log&quot;.
+ &quot;Debug&quot; produces very useful information for finding problems. There is usually no noteworthy performance impact but the file may become rather large. If this is a problem you may prefer the &quot;Info&quot; level for regular use. On the &quot;Error&quot; level the log file usually remains empty.
+ </string>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="0">
<widget class="QLabel" name="label_27">
<property name="text">
<string>Crash Dumps</string>
</property>
</widget>
</item>
- <item>
+ <item row="1" column="1">
<widget class="QComboBox" name="dumpsTypeBox">
<property name="toolTip">
<string>Decides which type of crash dumps are collected when injected processes crash.</string>
@@ -1240,88 +1509,38 @@ programs you are intentionally running.</string>
&quot;Full&quot; Even larger dumps with a full memory dump of the process.
</string>
</property>
- <item>
- <property name="text">
- <string>None</string>
- </property>
- </item>
- <item>
- <property name="text">
- <string>Mini (recommended)</string>
- </property>
- </item>
- <item>
- <property name="text">
- <string>Data</string>
- </property>
- </item>
- <item>
- <property name="text">
- <string>Full</string>
- </property>
- </item>
</widget>
</item>
- </layout>
- </item>
- <item row="4" column="0">
- <spacer name="verticalSpacer_10">
- <property name="orientation">
- <enum>Qt::Vertical</enum>
- </property>
- <property name="sizeType">
- <enum>QSizePolicy::Expanding</enum>
- </property>
- <property name="sizeHint" stdset="0">
- <size>
- <width>20</width>
- <height>232</height>
- </size>
- </property>
- </spacer>
- </item>
- <item row="0" column="0">
- <layout class="QHBoxLayout" name="horizontalLayout_6">
- <item>
- <widget class="QLabel" name="label_12">
+ <item row="2" column="0">
+ <widget class="QLabel" name="label_28">
<property name="text">
- <string>Log Level</string>
+ <string>Max Dumps To Keep</string>
</property>
</widget>
</item>
- <item>
- <widget class="QComboBox" name="logLevelBox">
+ <item row="2" column="1">
+ <widget class="QSpinBox" name="dumpsMaxEdit">
<property name="toolTip">
- <string>Decides the amount of data printed to &quot;ModOrganizer.log&quot;</string>
+ <string>Maximum number of crash dumps to keep on disk. Use 0 for unlimited.</string>
</property>
<property name="whatsThis">
<string>
- Decides the amount of data printed to &quot;ModOrganizer.log&quot;.
- &quot;Debug&quot; produces very useful information for finding problems. There is usually no noteworthy performance impact but the file may become rather large. If this is a problem you may prefer the &quot;Info&quot; level for regular use. On the &quot;Error&quot; level the log file usually remains empty.
+ Maximum number of crash dumps to keep on disk. Use 0 for unlimited.
+ Set &quot;Crash Dumps&quot; above to None to disable crash dump collection.
</string>
</property>
- <item>
- <property name="text">
- <string>Debug</string>
- </property>
- </item>
- <item>
- <property name="text">
- <string>Info (recommended)</string>
- </property>
- </item>
- <item>
- <property name="text">
- <string>Warning</string>
- </property>
- </item>
- <item>
- <property name="text">
- <string>Error</string>
- </property>
- </item>
</widget>
</item>
+ <item row="3" column="0">
+ <widget class="QLabel" name="label_32">
+ <property name="text">
+ <string>LOOT Log Level</string>
+ </property>
+ </widget>
+ </item>
+ <item row="3" column="1">
+ <widget class="QComboBox" name="lootLogLevel"/>
+ </item>
</layout>
</item>
</layout>
@@ -1340,13 +1559,32 @@ programs you are intentionally running.</string>
</item>
</layout>
</widget>
+ <customwidgets>
+ <customwidget>
+ <class>ColorTable</class>
+ <extends>QTableWidget</extends>
+ <header>colortable.h</header>
+ </customwidget>
+ <customwidget>
+ <class>LinkLabel</class>
+ <extends>QLabel</extends>
+ <header location="global">linklabel.h</header>
+ </customwidget>
+ </customwidgets>
<tabstops>
+ <tabstop>tabWidget</tabstop>
<tabstop>languageBox</tabstop>
<tabstop>styleBox</tabstop>
- <tabstop>logLevelBox</tabstop>
- <tabstop>usePrereleaseBox</tabstop>
- <tabstop>compactBox</tabstop>
+ <tabstop>centerDialogs</tabstop>
+ <tabstop>resetDialogsButton</tabstop>
<tabstop>categoriesBtn</tabstop>
+ <tabstop>showMetaBox</tabstop>
+ <tabstop>compactBox</tabstop>
+ <tabstop>checkForUpdates</tabstop>
+ <tabstop>usePrereleaseBox</tabstop>
+ <tabstop>colorTable</tabstop>
+ <tabstop>colorSeparatorsBox</tabstop>
+ <tabstop>resetColorsBtn</tabstop>
<tabstop>baseDirEdit</tabstop>
<tabstop>browseBaseDirBtn</tabstop>
<tabstop>downloadDirEdit</tabstop>
@@ -1359,8 +1597,18 @@ programs you are intentionally running.</string>
<tabstop>browseProfilesDirBtn</tabstop>
<tabstop>overwriteDirEdit</tabstop>
<tabstop>browseOverwriteDirBtn</tabstop>
- <tabstop>clearCacheButton</tabstop>
+ <tabstop>managedGameDirEdit</tabstop>
+ <tabstop>browseGameDirBtn</tabstop>
+ <tabstop>nexusConnect</tabstop>
+ <tabstop>nexusManualKey</tabstop>
+ <tabstop>nexusDisconnect</tabstop>
+ <tabstop>nexusLog</tabstop>
+ <tabstop>offlineBox</tabstop>
+ <tabstop>endorsementBox</tabstop>
+ <tabstop>proxyBox</tabstop>
+ <tabstop>hideAPICounterBox</tabstop>
<tabstop>associateButton</tabstop>
+ <tabstop>clearCacheButton</tabstop>
<tabstop>knownServersList</tabstop>
<tabstop>preferredServersList</tabstop>
<tabstop>steamUserEdit</tabstop>
@@ -1370,8 +1618,14 @@ programs you are intentionally running.</string>
<tabstop>pluginBlacklist</tabstop>
<tabstop>appIDEdit</tabstop>
<tabstop>mechanismBox</tabstop>
+ <tabstop>hideUncheckedBox</tabstop>
+ <tabstop>forceEnableBox</tabstop>
+ <tabstop>lockGUIBox</tabstop>
+ <tabstop>displayForeignBox</tabstop>
+ <tabstop>enableArchiveParsingBox</tabstop>
<tabstop>bsaDateBtn</tabstop>
- <tabstop>tabWidget</tabstop>
+ <tabstop>execBlacklistBtn</tabstop>
+ <tabstop>resetGeometryBtn</tabstop>
</tabstops>
<resources>
<include location="resources.qrc"/>
diff --git a/src/settingsdialogdiagnostics.cpp b/src/settingsdialogdiagnostics.cpp
new file mode 100644
index 00000000..74cadaa9
--- /dev/null
+++ b/src/settingsdialogdiagnostics.cpp
@@ -0,0 +1,109 @@
+#include "settingsdialogdiagnostics.h"
+#include "ui_settingsdialog.h"
+#include "appconfig.h"
+#include "organizercore.h"
+#include <log.h>
+
+using namespace MOBase;
+
+DiagnosticsSettingsTab::DiagnosticsSettingsTab(Settings& s, SettingsDialog& d)
+ : SettingsTab(s, d)
+{
+ setLogLevel();
+ setLootLogLevel();
+ setCrashDumpTypesBox();
+
+ ui->dumpsMaxEdit->setValue(settings().diagnostics().crashDumpsMax());
+
+ QString logsPath = qApp->property("dataPath").toString()
+ + "/" + QString::fromStdWString(AppConfig::logPath());
+
+ ui->diagnosticsExplainedLabel->setText(
+ ui->diagnosticsExplainedLabel->text()
+ .replace("LOGS_FULL_PATH", logsPath)
+ .replace("LOGS_DIR", QString::fromStdWString(AppConfig::logPath()))
+ .replace("DUMPS_FULL_PATH", QString::fromStdWString(OrganizerCore::crashDumpsPath()))
+ .replace("DUMPS_DIR", QString::fromStdWString(AppConfig::dumpsDir()))
+ );
+}
+
+void DiagnosticsSettingsTab::setLogLevel()
+{
+ ui->logLevelBox->clear();
+
+ ui->logLevelBox->addItem(QObject::tr("Debug"), log::Debug);
+ ui->logLevelBox->addItem(QObject::tr("Info (recommended)"), log::Info);
+ ui->logLevelBox->addItem(QObject::tr("Warning"), log::Warning);
+ ui->logLevelBox->addItem(QObject::tr("Error"), log::Error);
+
+ const auto sel = settings().diagnostics().logLevel();
+
+ for (int i=0; i<ui->logLevelBox->count(); ++i) {
+ if (ui->logLevelBox->itemData(i) == sel) {
+ ui->logLevelBox->setCurrentIndex(i);
+ break;
+ }
+ }
+}
+
+void DiagnosticsSettingsTab::setLootLogLevel()
+{
+ using L = lootcli::LogLevels;
+
+ auto v = [](L level) { return QVariant(static_cast<int>(level)); };
+
+ ui->lootLogLevel->clear();
+
+ ui->lootLogLevel->addItem(QObject::tr("Trace"), v(L::Trace));
+ ui->lootLogLevel->addItem(QObject::tr("Debug"), v(L::Debug));
+ ui->lootLogLevel->addItem(QObject::tr("Info (recommended)"), v(L::Info));
+ ui->lootLogLevel->addItem(QObject::tr("Warning"), v(L::Warning));
+ ui->lootLogLevel->addItem(QObject::tr("Error"), v(L::Error));
+
+ const auto sel = settings().diagnostics().lootLogLevel();
+
+ for (int i=0; i<ui->lootLogLevel->count(); ++i) {
+ if (ui->lootLogLevel->itemData(i) == v(sel)) {
+ ui->lootLogLevel->setCurrentIndex(i);
+ break;
+ }
+ }
+}
+
+void DiagnosticsSettingsTab::setCrashDumpTypesBox()
+{
+ ui->dumpsTypeBox->clear();
+
+ auto add = [&](auto&& text, auto&& type) {
+ ui->dumpsTypeBox->addItem(text, static_cast<int>(type));
+ };
+
+ add(QObject::tr("None"), CrashDumpsType::None);
+ add(QObject::tr("Mini (recommended)"), CrashDumpsType::Mini);
+ add(QObject::tr("Data"), CrashDumpsType::Data);
+ add(QObject::tr("Full"), CrashDumpsType::Full);
+
+ const auto current = static_cast<int>(
+ settings().diagnostics().crashDumpsType());
+
+ for (int i=0; i<ui->dumpsTypeBox->count(); ++i) {
+ if (ui->dumpsTypeBox->itemData(i) == current) {
+ ui->dumpsTypeBox->setCurrentIndex(i);
+ break;
+ }
+ }
+}
+
+void DiagnosticsSettingsTab::update()
+{
+ settings().diagnostics().setLogLevel(
+ static_cast<log::Levels>(ui->logLevelBox->currentData().toInt()));
+
+ settings().diagnostics().setCrashDumpsType(
+ static_cast<CrashDumpsType>(ui->dumpsTypeBox->currentData().toInt()));
+
+ settings().diagnostics().setCrashDumpsMax(ui->dumpsMaxEdit->value());
+
+ settings().diagnostics().setLootLogLevel(
+ static_cast<lootcli::LogLevels>(ui->lootLogLevel->currentData().toInt()));
+}
diff --git a/src/settingsdialogdiagnostics.h b/src/settingsdialogdiagnostics.h
new file mode 100644
index 00000000..e01ee22f
--- /dev/null
+++ b/src/settingsdialogdiagnostics.h
@@ -0,0 +1,20 @@
+#ifndef SETTINGSDIALOGDIAGNOSTICS_H
+#define SETTINGSDIALOGDIAGNOSTICS_H
+
+#include "settings.h"
+#include "settingsdialog.h"
+
+class DiagnosticsSettingsTab : public SettingsTab
+{
+public:
+ DiagnosticsSettingsTab(Settings& settings, SettingsDialog& dialog);
+
+ void update();
+
+private:
+ void setLogLevel();
+ void setLootLogLevel();
+ void setCrashDumpTypesBox();
+};
+
+#endif // SETTINGSDIALOGDIAGNOSTICS_H
diff --git a/src/settingsdialoggeneral.cpp b/src/settingsdialoggeneral.cpp
new file mode 100644
index 00000000..58871cee
--- /dev/null
+++ b/src/settingsdialoggeneral.cpp
@@ -0,0 +1,215 @@
+#include "settingsdialoggeneral.h"
+#include "ui_settingsdialog.h"
+#include "appconfig.h"
+#include "categoriesdialog.h"
+#include "colortable.h"
+#include <utility.h>
+#include <questionboxmemory.h>
+
+using namespace MOBase;
+
+GeneralSettingsTab::GeneralSettingsTab(Settings& s, SettingsDialog& d)
+ : SettingsTab(s, d)
+{
+ addLanguages();
+ selectLanguage();
+
+ addStyles();
+ selectStyle();
+
+ ui->colorTable->load(s);
+
+ ui->centerDialogs->setChecked(settings().geometry().centerDialogs());
+ ui->changeGameConfirmation->setChecked(settings().interface().showChangeGameConfirmation());
+ ui->doubleClickPreviews->setChecked(settings().interface().doubleClicksOpenPreviews());
+ ui->compactBox->setChecked(settings().interface().compactDownloads());
+ ui->showMetaBox->setChecked(settings().interface().metaDownloads());
+ ui->checkForUpdates->setChecked(settings().checkForUpdates());
+ ui->usePrereleaseBox->setChecked(settings().usePrereleases());
+ ui->colorSeparatorsBox->setChecked(settings().colors().colorSeparatorScrollbar());
+
+ QObject::connect(ui->exploreStyles, &QPushButton::clicked, [&]{ onExploreStyles(); });
+
+ QObject::connect(
+ ui->categoriesBtn, &QPushButton::clicked, [&]{ onEditCategories(); });
+
+ QObject::connect(
+ ui->resetColorsBtn, &QPushButton::clicked, [&]{ onResetColors(); });
+
+ QObject::connect(
+ ui->resetDialogsButton, &QPushButton::clicked, [&]{ onResetDialogs(); });
+}
+
+void GeneralSettingsTab::update()
+{
+ const QString oldLanguage = settings().interface().language();
+ const QString newLanguage = ui->languageBox->itemData(
+ ui->languageBox->currentIndex()).toString();
+
+ if (newLanguage != oldLanguage) {
+ settings().interface().setLanguage(newLanguage);
+ emit settings().languageChanged(newLanguage);
+ }
+
+ const QString oldStyle = settings().interface().styleName().value_or("");
+ const QString newStyle = ui->styleBox->itemData(
+ ui->styleBox->currentIndex()).toString();
+
+ if (oldStyle != newStyle) {
+ settings().interface().setStyleName(newStyle);
+ emit settings().styleChanged(newStyle);
+ }
+
+ ui->colorTable->commitColors();
+
+ settings().geometry().setCenterDialogs(ui->centerDialogs->isChecked());
+ settings().interface().setShowChangeGameConfirmation(ui->changeGameConfirmation->isChecked());
+ settings().interface().setDoubleClicksOpenPreviews(ui->doubleClickPreviews->isChecked());
+ settings().interface().setCompactDownloads(ui->compactBox->isChecked());
+ settings().interface().setMetaDownloads(ui->showMetaBox->isChecked());
+ settings().setCheckForUpdates(ui->checkForUpdates->isChecked());
+ settings().setUsePrereleases(ui->usePrereleaseBox->isChecked());
+ settings().colors().setColorSeparatorScrollbar(ui->colorSeparatorsBox->isChecked());
+}
+
+void GeneralSettingsTab::addLanguages()
+{
+ // matches the end of filenames for something like "_en.qm" or "_zh_CN.qm"
+ const QString pattern =
+ QString::fromStdWString(AppConfig::translationPrefix()) +
+ "_([a-z]{2,3}(_[A-Z]{2,2})?).qm";
+
+ const QRegExp exp(pattern);
+
+ QDirIterator iter(
+ QCoreApplication::applicationDirPath() + "/translations",
+ QDir::Files);
+
+ std::vector<std::pair<QString, QString>> languages;
+
+ while (iter.hasNext()) {
+ iter.next();
+
+ const QString file = iter.fileName();
+ if (!exp.exactMatch(file)) {
+ continue;
+ }
+
+ const QString languageCode = exp.cap(1);
+ const QLocale locale(languageCode);
+
+ QString languageString = QString("%1 (%2)")
+ .arg(locale.nativeLanguageName())
+ .arg(locale.nativeCountryName());
+
+ if (locale.language() == QLocale::Chinese) {
+ if (languageCode == "zh_TW") {
+ languageString = "Chinese (Traditional)";
+ } else {
+ languageString = "Chinese (Simplified)";
+ }
+ }
+
+ languages.push_back({languageString, exp.cap(1)});
+ }
+
+ if (!ui->languageBox->findText("English")) {
+ languages.push_back({QString("English"), QString("en_US")});
+ }
+
+ std::sort(languages.begin(), languages.end());
+
+ for (const auto& lang : languages) {
+ ui->languageBox->addItem(lang.first, lang.second);
+ }
+}
+
+void GeneralSettingsTab::selectLanguage()
+{
+ QString languageCode = settings().interface().language();
+ int currentID = ui->languageBox->findData(languageCode);
+ // I made a mess. :( Most languages are stored with only the iso country
+ // code (2 characters like "de") but chinese
+ // with the exact language variant (zh_TW) so I have to search for both
+ // variants
+ if (currentID == -1) {
+ currentID = ui->languageBox->findData(languageCode.mid(0, 2));
+ }
+ if (currentID != -1) {
+ ui->languageBox->setCurrentIndex(currentID);
+ }
+}
+
+void GeneralSettingsTab::addStyles()
+{
+ ui->styleBox->addItem("None", "");
+ for (auto&& key : QStyleFactory::keys()) {
+ ui->styleBox->addItem(key, key);
+ }
+
+ ui->styleBox->insertSeparator(ui->styleBox->count());
+
+ QDirIterator iter(
+ QCoreApplication::applicationDirPath() + "/" +
+ QString::fromStdWString(AppConfig::stylesheetsPath()),
+ QStringList("*.qss"),
+ QDir::Files);
+
+ while (iter.hasNext()) {
+ iter.next();
+
+ ui->styleBox->addItem(
+ iter.fileInfo().completeBaseName(),
+ iter.fileName());
+ }
+}
+
+void GeneralSettingsTab::selectStyle()
+{
+ const int currentID = ui->styleBox->findData(
+ settings().interface().styleName().value_or(""));
+
+ if (currentID != -1) {
+ ui->styleBox->setCurrentIndex(currentID);
+ }
+}
+
+void GeneralSettingsTab::resetDialogs()
+{
+ settings().widgets().resetQuestionButtons();
+}
+
+void GeneralSettingsTab::onExploreStyles()
+{
+ QString ssPath = QCoreApplication::applicationDirPath() + "/" + ToQString(AppConfig::stylesheetsPath());
+ shell::Explore(ssPath);
+}
+
+void GeneralSettingsTab::onEditCategories()
+{
+ CategoriesDialog dialog(&dialog());
+
+ if (dialog.exec() == QDialog::Accepted) {
+ dialog.commitChanges();
+ }
+}
+
+void GeneralSettingsTab::onResetColors()
+{
+ ui->colorTable->resetColors();
+}
+
+void GeneralSettingsTab::onResetDialogs()
+{
+ const auto r = QMessageBox::question(
+ parentWidget(),
+ QObject::tr("Confirm?"),
+ QObject::tr(
+ "This will reset all the choices you made to dialogs and make them all "
+ "visible again. Continue?"),
+ QMessageBox::Yes | QMessageBox::No);
+
+ if (r == QMessageBox::Yes) {
+ resetDialogs();
+ }
+}
diff --git a/src/settingsdialoggeneral.h b/src/settingsdialoggeneral.h
new file mode 100644
index 00000000..455edcaf
--- /dev/null
+++ b/src/settingsdialoggeneral.h
@@ -0,0 +1,29 @@
+#ifndef SETTINGSDIALOGGENERAL_H
+#define SETTINGSDIALOGGENERAL_H
+
+#include "settingsdialog.h"
+#include "settings.h"
+
+class GeneralSettingsTab : public SettingsTab
+{
+public:
+ GeneralSettingsTab(Settings& settings, SettingsDialog& dialog);
+
+ void update();
+
+private:
+ void addLanguages();
+ void selectLanguage();
+
+ void addStyles();
+ void selectStyle();
+
+ void resetDialogs();
+
+ void onExploreStyles();
+ void onEditCategories();
+ void onResetColors();
+ void onResetDialogs();
+};
+
+#endif // SETTINGSDIALOGGENERAL_H
diff --git a/src/settingsdialognexus.cpp b/src/settingsdialognexus.cpp
new file mode 100644
index 00000000..d49e0a33
--- /dev/null
+++ b/src/settingsdialognexus.cpp
@@ -0,0 +1,397 @@
+#include "settingsdialognexus.h"
+#include "ui_settingsdialog.h"
+#include "ui_nexusmanualkey.h"
+#include "nexusinterface.h"
+#include "serverinfo.h"
+#include "log.h"
+#include <utility.h>
+
+using namespace MOBase;
+
+template <typename T>
+class ServerItem : public QListWidgetItem {
+public:
+ ServerItem(const QString &text, int sortRole = Qt::DisplayRole, QListWidget *parent = 0, int type = Type)
+ : QListWidgetItem(text, parent, type), m_SortRole(sortRole) {}
+
+ virtual bool operator< ( const QListWidgetItem & other ) const {
+ return this->data(m_SortRole).value<T>() < other.data(m_SortRole).value<T>();
+ }
+private:
+ int m_SortRole;
+};
+
+
+class NexusManualKeyDialog : public QDialog
+{
+public:
+ NexusManualKeyDialog(QWidget* parent)
+ : QDialog(parent), ui(new Ui::NexusManualKeyDialog)
+ {
+ ui->setupUi(this);
+ ui->key->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont));
+
+ connect(ui->openBrowser, &QPushButton::clicked, [&]{ openBrowser(); });
+ connect(ui->paste, &QPushButton::clicked, [&]{ paste(); });
+ connect(ui->clear, &QPushButton::clicked, [&]{ clear(); });
+ }
+
+ void accept() override
+ {
+ m_key = ui->key->toPlainText();
+ QDialog::accept();
+ }
+
+ const QString& key() const
+ {
+ return m_key;
+ }
+
+ void openBrowser()
+ {
+ shell::Open(QUrl("https://www.nexusmods.com/users/myaccount?tab=api"));
+ }
+
+ void paste()
+ {
+ const auto text = QApplication::clipboard()->text();
+ if (!text.isEmpty()) {
+ ui->key->setPlainText(text);
+ }
+ }
+
+ void clear()
+ {
+ ui->key->clear();
+ }
+
+private:
+ std::unique_ptr<Ui::NexusManualKeyDialog> ui;
+ QString m_key;
+};
+
+
+NexusSettingsTab::NexusSettingsTab(Settings& s, SettingsDialog& d)
+ : SettingsTab(s, d)
+{
+ ui->offlineBox->setChecked(settings().network().offlineMode());
+ ui->proxyBox->setChecked(settings().network().useProxy());
+ ui->endorsementBox->setChecked(settings().nexus().endorsementIntegration());
+ ui->hideAPICounterBox->setChecked(settings().interface().hideAPICounter());
+
+ // display server preferences
+ for (const auto& server : s.network().servers()) {
+ QString descriptor = server.name();
+
+ if (!descriptor.compare("CDN", Qt::CaseInsensitive)) {
+ descriptor += QStringLiteral(" (automatic)");
+ }
+
+ const auto averageSpeed = server.averageSpeed();
+ if (averageSpeed > 0) {
+ descriptor += QString(" (%1)").arg(MOBase::localizedByteSpeed(averageSpeed));
+ }
+
+ QListWidgetItem *newItem = new ServerItem<int>(descriptor, Qt::UserRole + 1);
+
+ newItem->setData(Qt::UserRole, server.name());
+ newItem->setData(Qt::UserRole + 1, server.preferred());
+
+ if (server.preferred() > 0) {
+ ui->preferredServersList->addItem(newItem);
+ } else {
+ ui->knownServersList->addItem(newItem);
+ }
+
+ ui->preferredServersList->sortItems(Qt::DescendingOrder);
+ }
+
+ QObject::connect(ui->nexusConnect, &QPushButton::clicked, [&]{ on_nexusConnect_clicked(); });
+ QObject::connect(ui->nexusManualKey, &QPushButton::clicked, [&]{ on_nexusManualKey_clicked(); });
+ QObject::connect(ui->nexusDisconnect, &QPushButton::clicked, [&]{ on_nexusDisconnect_clicked(); });
+ QObject::connect(ui->clearCacheButton, &QPushButton::clicked, [&]{ on_clearCacheButton_clicked(); });
+ QObject::connect(ui->associateButton, &QPushButton::clicked, [&]{ on_associateButton_clicked(); });
+
+ if (settings().nexus().hasApiKey()) {
+ addNexusLog(QObject::tr("Connected."));
+ } else {
+ addNexusLog(QObject::tr("Not connected."));
+ }
+
+ updateNexusState();
+}
+
+void NexusSettingsTab::update()
+{
+ settings().network().setOfflineMode(ui->offlineBox->isChecked());
+ settings().network().setUseProxy(ui->proxyBox->isChecked());
+ settings().nexus().setEndorsementIntegration(ui->endorsementBox->isChecked());
+ settings().interface().setHideAPICounter(ui->hideAPICounterBox->isChecked());
+
+ auto servers = settings().network().servers();
+
+ // store server preference
+ for (int i = 0; i < ui->knownServersList->count(); ++i) {
+ const QString key = ui->knownServersList->item(i)->data(Qt::UserRole).toString();
+
+ bool found = false;
+
+ for (auto& server : servers) {
+ if (server.name() == key) {
+ server.setPreferred(0);
+ found = true;
+ break;
+ }
+ }
+
+ if (!found) {
+ log::error("while setting preferred to 0, server '{}' not found", key);
+ }
+ }
+
+ const int count = ui->preferredServersList->count();
+
+ for (int i = 0; i < count; ++i) {
+ const QString key = ui->preferredServersList->item(i)->data(Qt::UserRole).toString();
+ const int newPreferred = count - i;
+
+ bool found = false;
+
+ for (auto& server : servers) {
+
+ if (server.name() == key) {
+ server.setPreferred(newPreferred);
+ found = true;
+ break;
+ }
+ }
+
+ if (!found) {
+ log::error(
+ "while setting preference to {}, server '{}' not found",
+ newPreferred, key);
+ }
+ }
+
+ settings().network().updateServers(servers);
+}
+
+void NexusSettingsTab::on_nexusConnect_clicked()
+{
+ if (m_nexusLogin && m_nexusLogin->isActive()) {
+ m_nexusLogin->cancel();
+ return;
+ }
+
+ if (!m_nexusLogin) {
+ m_nexusLogin.reset(new NexusSSOLogin);
+
+ m_nexusLogin->keyChanged = [&](auto&& s){
+ onSSOKeyChanged(s);
+ };
+
+ m_nexusLogin->stateChanged = [&](auto&& s, auto&& e){
+ onSSOStateChanged(s, e);
+ };
+ }
+
+ ui->nexusLog->clear();
+ m_nexusLogin->start();
+ updateNexusState();
+}
+
+void NexusSettingsTab::on_nexusManualKey_clicked()
+{
+ if (m_nexusValidator && m_nexusValidator->isActive()) {
+ m_nexusValidator->cancel();
+ return;
+ }
+
+ NexusManualKeyDialog d(&dialog());
+ if (d.exec() != QDialog::Accepted) {
+ return;
+ }
+
+ const auto key = d.key();
+ if (key.isEmpty()) {
+ clearKey();
+ return;
+ }
+
+ ui->nexusLog->clear();
+ validateKey(key);
+}
+
+void NexusSettingsTab::on_nexusDisconnect_clicked()
+{
+ clearKey();
+ ui->nexusLog->clear();
+ addNexusLog(QObject::tr("Disconnected."));
+}
+
+void NexusSettingsTab::on_clearCacheButton_clicked()
+{
+ QDir(Settings::instance().paths().cache()).removeRecursively();
+ NexusInterface::instance(dialog().pluginContainer())->clearCache();
+}
+
+void NexusSettingsTab::on_associateButton_clicked()
+{
+ Settings::instance().nexus().registerAsNXMHandler(true);
+}
+
+void NexusSettingsTab::validateKey(const QString& key)
+{
+ if (!m_nexusValidator) {
+ m_nexusValidator.reset(new NexusKeyValidator(
+ *NexusInterface::instance(dialog().pluginContainer())->getAccessManager()));
+
+ m_nexusValidator->finished = [&](auto&& r, auto&& m, auto&& u) {
+ onValidatorFinished(r, m, u);
+ };
+ }
+
+ addNexusLog(QObject::tr("Checking API key..."));
+ m_nexusValidator->start(key, NexusKeyValidator::OneShot);
+}
+
+void NexusSettingsTab::onSSOKeyChanged(const QString& key)
+{
+ if (key.isEmpty()) {
+ clearKey();
+ } else {
+ addNexusLog(QObject::tr("Received API key."));
+ validateKey(key);
+ }
+}
+
+void NexusSettingsTab::onSSOStateChanged(NexusSSOLogin::States s, const QString& e)
+{
+ if (s != NexusSSOLogin::Finished) {
+ // finished state is handled in onSSOKeyChanged()
+ const auto log = NexusSSOLogin::stateToString(s, e);
+
+ for (auto&& line : log.split("\n")) {
+ addNexusLog(line);
+ }
+ }
+
+ updateNexusState();
+}
+
+void NexusSettingsTab::onValidatorFinished(
+ ValidationAttempt::Result r, const QString& message,
+ std::optional<APIUserAccount> user)
+{
+ if (user) {
+ NexusInterface::instance(dialog().pluginContainer())->setUserAccount(*user);
+ addNexusLog(QObject::tr("Received user acount information"));
+
+ if (setKey(user->apiKey())) {
+ addNexusLog(QObject::tr("Linked with Nexus successfully."));
+ } else {
+ addNexusLog(QObject::tr("Failed to set API key"));
+ }
+ } else {
+ if (message.isEmpty()) {
+ // shouldn't happen
+ addNexusLog("Unknown error");
+ } else {
+ addNexusLog(message);
+ }
+ }
+
+ updateNexusState();
+}
+
+void NexusSettingsTab::addNexusLog(const QString& s)
+{
+ ui->nexusLog->addItem(s);
+ ui->nexusLog->scrollToBottom();
+}
+
+bool NexusSettingsTab::setKey(const QString& key)
+{
+ dialog().setExitNeeded(Exit::Restart);
+ const bool ret = settings().nexus().setApiKey(key);
+ updateNexusState();
+ return ret;
+}
+
+bool NexusSettingsTab::clearKey()
+{
+ dialog().setExitNeeded(Exit::Restart);
+ const auto ret = settings().nexus().clearApiKey();
+
+ NexusInterface::instance(dialog().pluginContainer())->getAccessManager()->clearApiKey();
+ updateNexusState();
+
+ return ret;
+}
+
+void NexusSettingsTab::updateNexusState()
+{
+ updateNexusButtons();
+ updateNexusData();
+}
+
+void NexusSettingsTab::updateNexusButtons()
+{
+ if (m_nexusLogin && m_nexusLogin->isActive()) {
+ // api key is in the process of being retrieved
+ ui->nexusConnect->setText(QObject::tr("Cancel"));
+ ui->nexusConnect->setEnabled(true);
+ ui->nexusDisconnect->setEnabled(false);
+ ui->nexusManualKey->setText(QObject::tr("Enter API Key Manually"));
+ ui->nexusManualKey->setEnabled(false);
+ }
+ else if (m_nexusValidator && m_nexusValidator->isActive()) {
+ // api key is in the process of being tested
+ ui->nexusConnect->setText(QObject::tr("Connect to Nexus"));
+ ui->nexusConnect->setEnabled(false);
+ ui->nexusDisconnect->setEnabled(false);
+ ui->nexusManualKey->setText(QObject::tr("Cancel"));
+ ui->nexusManualKey->setEnabled(true);
+ }
+ else if (settings().nexus().hasApiKey()) {
+ // api key is present
+ ui->nexusConnect->setText(QObject::tr("Connect to Nexus"));
+ ui->nexusConnect->setEnabled(false);
+ ui->nexusDisconnect->setEnabled(true);
+ ui->nexusManualKey->setText(QObject::tr("Enter API Key Manually"));
+ ui->nexusManualKey->setEnabled(false);
+ } else {
+ // api key not present
+ ui->nexusConnect->setText(QObject::tr("Connect to Nexus"));
+ ui->nexusConnect->setEnabled(true);
+ ui->nexusDisconnect->setEnabled(false);
+ ui->nexusManualKey->setText(QObject::tr("Enter API Key Manually"));
+ ui->nexusManualKey->setEnabled(true);
+ }
+}
+
+void NexusSettingsTab::updateNexusData()
+{
+ const auto user = NexusInterface::instance(dialog().pluginContainer())
+ ->getAPIUserAccount();
+
+ if (user.isValid()) {
+ ui->nexusUserID->setText(user.id());
+ ui->nexusName->setText(user.name());
+ ui->nexusAccount->setText(localizedUserAccountType(user.type()));
+
+ ui->nexusDailyRequests->setText(QString("%1/%2")
+ .arg(user.limits().remainingDailyRequests)
+ .arg(user.limits().maxDailyRequests));
+
+ ui->nexusHourlyRequests->setText(QString("%1/%2")
+ .arg(user.limits().remainingHourlyRequests)
+ .arg(user.limits().maxHourlyRequests));
+ } else {
+ ui->nexusUserID->setText(QObject::tr("N/A"));
+ ui->nexusName->setText(QObject::tr("N/A"));
+ ui->nexusAccount->setText(QObject::tr("N/A"));
+ ui->nexusDailyRequests->setText(QObject::tr("N/A"));
+ ui->nexusHourlyRequests->setText(QObject::tr("N/A"));
+ }
+}
diff --git a/src/settingsdialognexus.h b/src/settingsdialognexus.h
new file mode 100644
index 00000000..2cb1cc1e
--- /dev/null
+++ b/src/settingsdialognexus.h
@@ -0,0 +1,42 @@
+#ifndef SETTINGSDIALOGNEXUS_H
+#define SETTINGSDIALOGNEXUS_H
+
+#include "settings.h"
+#include "settingsdialog.h"
+#include "nxmaccessmanager.h"
+
+class NexusSettingsTab : public SettingsTab
+{
+public:
+ NexusSettingsTab(Settings& settings, SettingsDialog& dialog);
+ void update();
+
+private:
+ std::unique_ptr<NexusSSOLogin> m_nexusLogin;
+ std::unique_ptr<NexusKeyValidator> m_nexusValidator;
+
+ void on_nexusConnect_clicked();
+ void on_nexusManualKey_clicked();
+ void on_nexusDisconnect_clicked();
+ void on_clearCacheButton_clicked();
+ void on_associateButton_clicked();
+
+ void validateKey(const QString& key);
+ bool setKey(const QString& key);
+ bool clearKey();
+
+ void updateNexusState();
+ void updateNexusButtons();
+ void updateNexusData();
+
+ void onSSOKeyChanged(const QString& key);
+ void onSSOStateChanged(NexusSSOLogin::States s, const QString& e);
+
+ void onValidatorFinished(
+ ValidationAttempt::Result r, const QString& message,
+ std::optional<APIUserAccount> useR);
+
+ void addNexusLog(const QString& s);
+};
+
+#endif // SETTINGSDIALOGNEXUS_H
diff --git a/src/settingsdialogpaths.cpp b/src/settingsdialogpaths.cpp
new file mode 100644
index 00000000..c6fd40a7
--- /dev/null
+++ b/src/settingsdialogpaths.cpp
@@ -0,0 +1,215 @@
+#include "settingsdialogpaths.h"
+#include "ui_settingsdialog.h"
+#include "appconfig.h"
+#include <iplugingame.h>
+
+PathsSettingsTab::PathsSettingsTab(Settings& s, SettingsDialog& d)
+ : SettingsTab(s, d)
+{
+ ui->baseDirEdit->setText(settings().paths().base());
+
+ ui->managedGameDirEdit->setText(
+ settings().game().plugin()->gameDirectory().absoluteFilePath(
+ settings().game().plugin()->binaryName()));
+
+ QString basePath = settings().paths().base();
+ QDir baseDir(basePath);
+
+ for (const auto &dir : {
+ std::make_pair(ui->downloadDirEdit, settings().paths().downloads(false)),
+ std::make_pair(ui->modDirEdit, settings().paths().mods(false)),
+ std::make_pair(ui->cacheDirEdit, settings().paths().cache(false)),
+ std::make_pair(ui->profilesDirEdit, settings().paths().profiles(false)),
+ std::make_pair(ui->overwriteDirEdit, settings().paths().overwrite(false))
+ }) {
+
+ QString storePath = baseDir.relativeFilePath(dir.second);
+ storePath = dir.second;
+ dir.first->setText(storePath);
+ }
+
+ QObject::connect(ui->browseBaseDirBtn, &QPushButton::clicked, [&]{ on_browseBaseDirBtn_clicked(); });
+ QObject::connect(ui->browseCacheDirBtn, &QPushButton::clicked, [&]{ on_browseCacheDirBtn_clicked(); });
+ QObject::connect(ui->browseDownloadDirBtn, &QPushButton::clicked, [&]{ on_browseDownloadDirBtn_clicked(); });
+ QObject::connect(ui->browseGameDirBtn, &QPushButton::clicked, [&]{ on_browseGameDirBtn_clicked(); });
+ QObject::connect(ui->browseModDirBtn, &QPushButton::clicked, [&]{ on_browseModDirBtn_clicked(); });
+ QObject::connect(ui->browseOverwriteDirBtn, &QPushButton::clicked, [&]{ on_browseOverwriteDirBtn_clicked(); });
+ QObject::connect(ui->browseProfilesDirBtn, &QPushButton::clicked, [&]{ on_browseProfilesDirBtn_clicked(); });
+
+ QObject::connect(ui->baseDirEdit, &QLineEdit::editingFinished, [&]{ on_baseDirEdit_editingFinished(); });
+ QObject::connect(ui->cacheDirEdit, &QLineEdit::editingFinished, [&]{ on_cacheDirEdit_editingFinished(); });
+ QObject::connect(ui->downloadDirEdit, &QLineEdit::editingFinished, [&]{ on_downloadDirEdit_editingFinished(); });
+ QObject::connect(ui->modDirEdit, &QLineEdit::editingFinished, [&]{ on_modDirEdit_editingFinished(); });
+ QObject::connect(ui->overwriteDirEdit, &QLineEdit::editingFinished, [&]{ on_overwriteDirEdit_editingFinished(); });
+ QObject::connect(ui->profilesDirEdit, &QLineEdit::editingFinished, [&]{ on_profilesDirEdit_editingFinished(); });
+}
+
+void PathsSettingsTab::update()
+{
+ using Setter = void (PathSettings::*)(const QString&);
+ using Directory = std::tuple<QString, Setter, std::wstring>;
+
+ QString basePath = settings().paths().base();
+
+ for (const Directory &dir :{
+ Directory{ui->downloadDirEdit->text(), &PathSettings::setDownloads, AppConfig::downloadPath()},
+ Directory{ui->cacheDirEdit->text(), &PathSettings::setCache, AppConfig::cachePath()},
+ Directory{ui->modDirEdit->text(), &PathSettings::setMods, AppConfig::modsPath()},
+ Directory{ui->overwriteDirEdit->text(), &PathSettings::setOverwrite, AppConfig::overwritePath()},
+ Directory{ui->profilesDirEdit->text(), &PathSettings::setProfiles, AppConfig::profilesPath()}
+ }) {
+ QString path;
+ Setter setter;
+ std::wstring defaultName;
+ std::tie(path, setter, defaultName) = dir;
+
+ QString realPath = path;
+ realPath.replace("%BASE_DIR%", ui->baseDirEdit->text());
+
+ if (!QDir(realPath).exists()) {
+ if (!QDir().mkpath(realPath)) {
+ QMessageBox::warning(parentWidget(), QObject::tr("Error"),
+ QObject::tr("Failed to create \"%1\", you may not have the "
+ "necessary permissions. Path remains unchanged.")
+ .arg(realPath));
+
+ continue;
+ }
+ }
+
+ if (QFileInfo(realPath) != QFileInfo(basePath + "/" + QString::fromStdWString(defaultName))) {
+ (settings().paths().*setter)(path);
+ } else {
+ (settings().paths().*setter)("");
+ }
+ }
+
+ if (QFileInfo(ui->baseDirEdit->text()) != QFileInfo(qApp->property("dataPath").toString())) {
+ settings().paths().setBase(ui->baseDirEdit->text());
+ } else {
+ settings().paths().setBase("");
+ }
+
+ QFileInfo oldGameExe(
+ settings().game().plugin()->gameDirectory().absoluteFilePath(
+ settings().game().plugin()->binaryName()));
+
+ QFileInfo newGameExe(ui->managedGameDirEdit->text());
+
+ if (oldGameExe != newGameExe) {
+ settings().game().setDirectory(newGameExe.absolutePath());
+ }
+}
+
+void PathsSettingsTab::on_browseBaseDirBtn_clicked()
+{
+ QString temp = QFileDialog::getExistingDirectory(
+ &dialog(), QObject::tr("Select base directory"), ui->baseDirEdit->text());
+ if (!temp.isEmpty()) {
+ ui->baseDirEdit->setText(temp);
+ }
+}
+
+void PathsSettingsTab::on_browseDownloadDirBtn_clicked()
+{
+ QString searchPath = ui->downloadDirEdit->text();
+ searchPath.replace("%BASE_DIR%", ui->baseDirEdit->text());
+
+ QString temp = QFileDialog::getExistingDirectory(&dialog(), QObject::tr("Select download directory"), searchPath);
+ if (!temp.isEmpty()) {
+ ui->downloadDirEdit->setText(temp);
+ }
+}
+
+void PathsSettingsTab::on_browseModDirBtn_clicked()
+{
+ QString searchPath = ui->modDirEdit->text();
+ searchPath.replace("%BASE_DIR%", ui->baseDirEdit->text());
+
+ QString temp = QFileDialog::getExistingDirectory(&dialog(), QObject::tr("Select mod directory"), searchPath);
+ if (!temp.isEmpty()) {
+ ui->modDirEdit->setText(temp);
+ }
+}
+
+void PathsSettingsTab::on_browseCacheDirBtn_clicked()
+{
+ QString searchPath = ui->cacheDirEdit->text();
+ searchPath.replace("%BASE_DIR%", ui->baseDirEdit->text());
+
+ QString temp = QFileDialog::getExistingDirectory(&dialog(), QObject::tr("Select cache directory"), searchPath);
+ if (!temp.isEmpty()) {
+ ui->cacheDirEdit->setText(temp);
+ }
+}
+
+void PathsSettingsTab::on_browseProfilesDirBtn_clicked()
+{
+ QString searchPath = ui->profilesDirEdit->text();
+ searchPath.replace("%BASE_DIR%", ui->baseDirEdit->text());
+
+ QString temp = QFileDialog::getExistingDirectory(&dialog(), QObject::tr("Select profiles directory"), searchPath);
+ if (!temp.isEmpty()) {
+ ui->profilesDirEdit->setText(temp);
+ }
+}
+
+void PathsSettingsTab::on_browseOverwriteDirBtn_clicked()
+{
+ QString searchPath = ui->overwriteDirEdit->text();
+ searchPath.replace("%BASE_DIR%", ui->baseDirEdit->text());
+
+ QString temp = QFileDialog::getExistingDirectory(&dialog(), QObject::tr("Select overwrite directory"), searchPath);
+ if (!temp.isEmpty()) {
+ ui->overwriteDirEdit->setText(temp);
+ }
+}
+
+void PathsSettingsTab::on_browseGameDirBtn_clicked()
+{
+ QFileInfo oldGameExe(ui->managedGameDirEdit->text());
+
+ QString temp = QFileDialog::getOpenFileName(&dialog(), QObject::tr("Select game executable"), oldGameExe.absolutePath(), oldGameExe.fileName());
+ if (!temp.isEmpty()) {
+ ui->managedGameDirEdit->setText(temp);
+ }
+}
+
+void PathsSettingsTab::on_baseDirEdit_editingFinished()
+{
+ normalizePath(ui->baseDirEdit);
+}
+
+void PathsSettingsTab::on_downloadDirEdit_editingFinished()
+{
+ normalizePath(ui->downloadDirEdit);
+}
+
+void PathsSettingsTab::on_modDirEdit_editingFinished()
+{
+ normalizePath(ui->modDirEdit);
+}
+
+void PathsSettingsTab::on_cacheDirEdit_editingFinished()
+{
+ normalizePath(ui->cacheDirEdit);
+}
+
+void PathsSettingsTab::on_profilesDirEdit_editingFinished()
+{
+ normalizePath(ui->profilesDirEdit);
+}
+
+void PathsSettingsTab::on_overwriteDirEdit_editingFinished()
+{
+ normalizePath(ui->overwriteDirEdit);
+}
+
+void PathsSettingsTab::normalizePath(QLineEdit *lineEdit)
+{
+ QString text = lineEdit->text();
+ while (text.endsWith('/') || text.endsWith('\\')) {
+ text.chop(1);
+ }
+ lineEdit->setText(text);
+}
diff --git a/src/settingsdialogpaths.h b/src/settingsdialogpaths.h
new file mode 100644
index 00000000..a2073188
--- /dev/null
+++ b/src/settingsdialogpaths.h
@@ -0,0 +1,32 @@
+#ifndef SETTINGSDIALOGPATHS_H
+#define SETTINGSDIALOGPATHS_H
+
+#include "settings.h"
+#include "settingsdialog.h"
+
+class PathsSettingsTab : public SettingsTab
+{
+public:
+ PathsSettingsTab(Settings& settings, SettingsDialog& dialog);
+ void update();
+
+private:
+ void on_browseBaseDirBtn_clicked();
+ void on_browseCacheDirBtn_clicked();
+ void on_browseDownloadDirBtn_clicked();
+ void on_browseGameDirBtn_clicked();
+ void on_browseModDirBtn_clicked();
+ void on_browseOverwriteDirBtn_clicked();
+ void on_browseProfilesDirBtn_clicked();
+
+ void on_baseDirEdit_editingFinished();
+ void on_cacheDirEdit_editingFinished();
+ void on_downloadDirEdit_editingFinished();
+ void on_modDirEdit_editingFinished();
+ void on_overwriteDirEdit_editingFinished();
+ void on_profilesDirEdit_editingFinished();
+
+ void normalizePath(QLineEdit *lineEdit);
+};
+
+#endif // SETTINGSDIALOGPATHS_H
diff --git a/src/settingsdialogplugins.cpp b/src/settingsdialogplugins.cpp
new file mode 100644
index 00000000..c84d0556
--- /dev/null
+++ b/src/settingsdialogplugins.cpp
@@ -0,0 +1,119 @@
+#include "settingsdialogplugins.h"
+#include "ui_settingsdialog.h"
+#include "noeditdelegate.h"
+#include <iplugin.h>
+
+using MOBase::IPlugin;
+
+PluginsSettingsTab::PluginsSettingsTab(Settings& s, SettingsDialog& d)
+ : SettingsTab(s, d)
+{
+ ui->pluginSettingsList->setStyleSheet("QTreeWidget::item {padding-right: 10px;}");
+
+ // display plugin settings
+ QSet<QString> handledNames;
+ for (IPlugin *plugin : settings().plugins().plugins()) {
+ if (handledNames.contains(plugin->name()))
+ continue;
+ QListWidgetItem *listItem = new QListWidgetItem(plugin->name(), ui->pluginsList);
+ listItem->setData(Qt::UserRole, QVariant::fromValue((void*)plugin));
+ listItem->setData(Qt::UserRole + 1, settings().plugins().settings(plugin->name()));
+ listItem->setData(Qt::UserRole + 2, settings().plugins().descriptions(plugin->name()));
+ ui->pluginsList->addItem(listItem);
+ handledNames.insert(plugin->name());
+ }
+
+ // display plugin blacklist
+ for (const QString &pluginName : settings().plugins().blacklist()) {
+ ui->pluginBlacklist->addItem(pluginName);
+ }
+
+ QObject::connect(
+ ui->pluginsList, &QListWidget::currentItemChanged,
+ [&](auto* current, auto* previous) { on_pluginsList_currentItemChanged(current, previous); });
+
+ QShortcut *delShortcut = new QShortcut(
+ QKeySequence(Qt::Key_Delete), ui->pluginBlacklist);
+ QObject::connect(delShortcut, &QShortcut::activated, &dialog(), [&]{ deleteBlacklistItem(); });
+}
+
+void PluginsSettingsTab::update()
+{
+ // transfer plugin settings to in-memory structure
+ for (int i = 0; i < ui->pluginsList->count(); ++i) {
+ QListWidgetItem *item = ui->pluginsList->item(i);
+ settings().plugins().setSettings(
+ item->text(), item->data(Qt::UserRole + 1).toMap());
+ }
+
+ // set plugin blacklist
+ QStringList names;
+ for (QListWidgetItem *item : ui->pluginBlacklist->findItems("*", Qt::MatchWildcard)) {
+ names.push_back(item->text());
+ }
+
+ settings().plugins().setBlacklist(names);
+
+ settings().plugins().save();
+}
+
+void PluginsSettingsTab::closing()
+{
+ storeSettings(ui->pluginsList->currentItem());
+}
+
+void PluginsSettingsTab::on_pluginsList_currentItemChanged(QListWidgetItem *current, QListWidgetItem *previous)
+{
+ storeSettings(previous);
+
+ ui->pluginSettingsList->clear();
+ IPlugin *plugin = static_cast<IPlugin*>(current->data(Qt::UserRole).value<void*>());
+ ui->authorLabel->setText(plugin->author());
+ ui->versionLabel->setText(plugin->version().canonicalString());
+ ui->descriptionLabel->setText(plugin->description());
+
+ QVariantMap settings = current->data(Qt::UserRole + 1).toMap();
+ QVariantMap descriptions = current->data(Qt::UserRole + 2).toMap();
+ ui->pluginSettingsList->setEnabled(settings.count() != 0);
+ for (auto iter = settings.begin(); iter != settings.end(); ++iter) {
+ QTreeWidgetItem *newItem = new QTreeWidgetItem(QStringList(iter.key()));
+ QVariant value = *iter;
+ QString description;
+ {
+ auto descriptionIter = descriptions.find(iter.key());
+ if (descriptionIter != descriptions.end()) {
+ description = descriptionIter->toString();
+ }
+ }
+
+ ui->pluginSettingsList->setItemDelegateForColumn(0, new NoEditDelegate());
+ newItem->setData(1, Qt::DisplayRole, value);
+ newItem->setData(1, Qt::EditRole, value);
+ newItem->setToolTip(1, description);
+
+ newItem->setFlags(newItem->flags() | Qt::ItemIsEditable);
+ ui->pluginSettingsList->addTopLevelItem(newItem);
+ }
+
+ ui->pluginSettingsList->resizeColumnToContents(0);
+ ui->pluginSettingsList->resizeColumnToContents(1);
+}
+
+void PluginsSettingsTab::deleteBlacklistItem()
+{
+ ui->pluginBlacklist->takeItem(ui->pluginBlacklist->currentIndex().row());
+}
+
+void PluginsSettingsTab::storeSettings(QListWidgetItem *pluginItem)
+{
+ if (pluginItem != nullptr) {
+ QVariantMap settings = pluginItem->data(Qt::UserRole + 1).toMap();
+
+ for (int i = 0; i < ui->pluginSettingsList->topLevelItemCount(); ++i) {
+ const QTreeWidgetItem *item = ui->pluginSettingsList->topLevelItem(i);
+ settings[item->text(0)] = item->data(1, Qt::DisplayRole);
+ }
+
+ pluginItem->setData(Qt::UserRole + 1, settings);
+ }
+}
diff --git a/src/settingsdialogplugins.h b/src/settingsdialogplugins.h
new file mode 100644
index 00000000..8e2dae2a
--- /dev/null
+++ b/src/settingsdialogplugins.h
@@ -0,0 +1,21 @@
+#ifndef SETTINGSDIALOGPLUGINS_H
+#define SETTINGSDIALOGPLUGINS_H
+
+#include "settings.h"
+#include "settingsdialog.h"
+
+class PluginsSettingsTab : public SettingsTab
+{
+public:
+ PluginsSettingsTab(Settings& settings, SettingsDialog& dialog);
+
+ void update();
+ void closing() override;
+
+private:
+ void on_pluginsList_currentItemChanged(QListWidgetItem *current, QListWidgetItem *previous);
+ void deleteBlacklistItem();
+ void storeSettings(QListWidgetItem *pluginItem);
+};
+
+#endif // SETTINGSDIALOGPLUGINS_H
diff --git a/src/settingsdialogsteam.cpp b/src/settingsdialogsteam.cpp
new file mode 100644
index 00000000..3c4c5de6
--- /dev/null
+++ b/src/settingsdialogsteam.cpp
@@ -0,0 +1,17 @@
+#include "settingsdialogsteam.h"
+#include "ui_settingsdialog.h"
+
+SteamSettingsTab::SteamSettingsTab(Settings& s, SettingsDialog& d)
+ : SettingsTab(s, d)
+{
+ QString username, password;
+ settings().steam().login(username, password);
+
+ ui->steamUserEdit->setText(username);
+ ui->steamPassEdit->setText(password);
+}
+
+void SteamSettingsTab::update()
+{
+ settings().steam().setLogin(ui->steamUserEdit->text(), ui->steamPassEdit->text());
+}
diff --git a/src/settingsdialogsteam.h b/src/settingsdialogsteam.h
new file mode 100644
index 00000000..6a3d75f4
--- /dev/null
+++ b/src/settingsdialogsteam.h
@@ -0,0 +1,14 @@
+#ifndef SETTINGSDIALOGSTEAM_H
+#define SETTINGSDIALOGSTEAM_H
+
+#include "settings.h"
+#include "settingsdialog.h"
+
+class SteamSettingsTab : public SettingsTab
+{
+public:
+ SteamSettingsTab(Settings& settings, SettingsDialog& dialog);
+ void update();
+};
+
+#endif // SETTINGSDIALOGSTEAM_H
diff --git a/src/settingsdialogworkarounds.cpp b/src/settingsdialogworkarounds.cpp
new file mode 100644
index 00000000..1c5fbe26
--- /dev/null
+++ b/src/settingsdialogworkarounds.cpp
@@ -0,0 +1,136 @@
+#include "settingsdialogworkarounds.h"
+#include "ui_settingsdialog.h"
+#include "spawn.h"
+#include "settings.h"
+#include <report.h>
+#include <iplugingame.h>
+
+WorkaroundsSettingsTab::WorkaroundsSettingsTab(Settings& s, SettingsDialog& d)
+ : SettingsTab(s, d)
+{
+ ui->appIDEdit->setText(settings().steam().appID());
+
+ LoadMechanism::EMechanism mechanismID = settings().game().loadMechanismType();
+ int index = 0;
+
+ if (settings().game().loadMechanism().isDirectLoadingSupported()) {
+ ui->mechanismBox->addItem(QObject::tr("Mod Organizer"), LoadMechanism::LOAD_MODORGANIZER);
+ if (mechanismID == LoadMechanism::LOAD_MODORGANIZER) {
+ index = ui->mechanismBox->count() - 1;
+ }
+ }
+
+ ui->mechanismBox->setCurrentIndex(index);
+
+ ui->hideUncheckedBox->setChecked(settings().game().hideUncheckedPlugins());
+ ui->forceEnableBox->setChecked(settings().game().forceEnableCoreFiles());
+ ui->displayForeignBox->setChecked(settings().interface().displayForeign());
+ ui->lockGUIBox->setChecked(settings().interface().lockGUI());
+ ui->enableArchiveParsingBox->setChecked(settings().archiveParsing());
+
+ m_ExecutableBlacklist = settings().executablesBlacklist();
+
+ QObject::connect(ui->bsaDateBtn, &QPushButton::clicked, [&]{ on_bsaDateBtn_clicked(); });
+ QObject::connect(ui->execBlacklistBtn, &QPushButton::clicked, [&]{ on_execBlacklistBtn_clicked(); });
+ QObject::connect(ui->resetGeometryBtn, &QPushButton::clicked, [&]{ on_resetGeometryBtn_clicked(); });
+}
+
+void WorkaroundsSettingsTab::update()
+{
+ if (ui->appIDEdit->text() != settings().game().plugin()->steamAPPId()) {
+ settings().steam().setAppID(ui->appIDEdit->text());
+ } else {
+ settings().steam().setAppID("");
+ }
+
+ settings().game().setLoadMechanism(static_cast<LoadMechanism::EMechanism>(
+ ui->mechanismBox->itemData(ui->mechanismBox->currentIndex()).toInt()));
+
+ settings().game().setHideUncheckedPlugins(ui->hideUncheckedBox->isChecked());
+ settings().game().setForceEnableCoreFiles(ui->forceEnableBox->isChecked());
+ settings().interface().setDisplayForeign(ui->displayForeignBox->isChecked());
+ settings().interface().setLockGUI(ui->lockGUIBox->isChecked());
+ settings().setArchiveParsing(ui->enableArchiveParsingBox->isChecked());
+ settings().setExecutablesBlacklist(m_ExecutableBlacklist);
+}
+
+bool WorkaroundsSettingsTab::changeBlacklistNow(
+ QWidget* parent, Settings& settings)
+{
+ const auto current = settings.executablesBlacklist();
+
+ if (auto s=changeBlacklistLater(parent, current)) {
+ settings.setExecutablesBlacklist(*s);
+ return true;
+ }
+
+ return false;
+}
+
+std::optional<QString> WorkaroundsSettingsTab::changeBlacklistLater(
+ QWidget* parent, const QString& current)
+{
+ bool ok = false;
+
+ QString result = QInputDialog::getMultiLineText(
+ parent,
+ QObject::tr("Executables Blacklist"),
+ QObject::tr("Enter one executable per line to be blacklisted from the virtual file system.\n"
+ "Mods and other virtualized files will not be visible to these executables and\n"
+ "any executables launched by them.\n\n"
+ "Example:\n"
+ " Chrome.exe\n"
+ " Firefox.exe"),
+ current.split(";").join("\n"),
+ &ok
+ );
+
+ if (!ok) {
+ return {};
+ }
+
+ QStringList blacklist;
+ for (auto exec : result.split("\n")) {
+ if (exec.trimmed().endsWith(".exe", Qt::CaseInsensitive)) {
+ blacklist << exec.trimmed();
+ }
+ }
+
+ return blacklist.join(";");
+}
+
+void WorkaroundsSettingsTab::on_execBlacklistBtn_clicked()
+{
+ if (auto s=changeBlacklistLater(parentWidget(), m_ExecutableBlacklist)) {
+ m_ExecutableBlacklist = *s;
+ }
+}
+
+void WorkaroundsSettingsTab::on_bsaDateBtn_clicked()
+{
+ const auto* game = qApp->property("managed_game").value<MOBase::IPluginGame*>();
+ QDir dir = game->dataDirectory();
+
+ helper::backdateBSAs(
+ parentWidget(),
+ qApp->applicationDirPath().toStdWString(),
+ dir.absolutePath().toStdWString());
+}
+
+void WorkaroundsSettingsTab::on_resetGeometryBtn_clicked()
+{
+ const auto r = MOBase::TaskDialog(parentWidget())
+ .title(QObject::tr("Restart Mod Organizer"))
+ .main(QObject::tr("Restart Mod Organizer"))
+ .content(QObject::tr("Geometries will be reset to their default values."))
+ .icon(QMessageBox::Question)
+ .button({QObject::tr("Restart Mod Organizer"), QMessageBox::Ok})
+ .button({QObject::tr("Cancel"), QMessageBox::Cancel})
+ .exec();
+
+ if (r == QMessageBox::Ok) {
+ settings().geometry().requestReset();
+ ExitModOrganizer(Exit::Restart);
+ dialog().close();
+ }
+}
diff --git a/src/settingsdialogworkarounds.h b/src/settingsdialogworkarounds.h
new file mode 100644
index 00000000..cffc54a0
--- /dev/null
+++ b/src/settingsdialogworkarounds.h
@@ -0,0 +1,33 @@
+#ifndef SETTINGSDIALOGWORKAROUNDS_H
+#define SETTINGSDIALOGWORKAROUNDS_H
+
+#include "settings.h"
+#include "settingsdialog.h"
+
+class WorkaroundsSettingsTab : public SettingsTab
+{
+public:
+ WorkaroundsSettingsTab(Settings& settings, SettingsDialog& dialog);
+
+ // shows the blacklist dialog from the given settings, and changes the
+ // settings when the user accepts it
+ //
+ static bool changeBlacklistNow(QWidget* parent, Settings& settings);
+
+ // shows the blacklist dialog from the given string and returns the new
+ // blacklist if the user accepted it
+ //
+ static std::optional<QString> changeBlacklistLater(
+ QWidget* parent, const QString& current);
+
+ void update();
+
+private:
+ QString m_ExecutableBlacklist;
+
+ void on_bsaDateBtn_clicked();
+ void on_execBlacklistBtn_clicked();
+ void on_resetGeometryBtn_clicked();
+};
+
+#endif // SETTINGSDIALOGWORKAROUNDS_H
diff --git a/src/settingsutilities.cpp b/src/settingsutilities.cpp
new file mode 100644
index 00000000..db7c1818
--- /dev/null
+++ b/src/settingsutilities.cpp
@@ -0,0 +1,331 @@
+#include "settingsutilities.h"
+#include "expanderwidget.h"
+#include <utility.h>
+
+using namespace MOBase;
+
+bool shouldLogSetting(const QString& displayName)
+{
+ // don't log Geometry/ and Widgets/, too noisy and not very useful
+ static const QStringList ignorePrefixes = {"Geometry/", "Widgets/"};
+
+ for (auto&& prefix : ignorePrefixes) {
+ if (displayName.startsWith(prefix, Qt::CaseInsensitive)) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+void logRemoval(const QString& name)
+{
+ if (!shouldLogSetting(name)) {
+ return;
+ }
+
+ log::debug("setting '{}' removed", name);
+}
+
+
+QString settingName(const QString& section, const QString& key)
+{
+ if (section.isEmpty()) {
+ return key;
+ } else if (key.isEmpty()) {
+ return section;
+ } else {
+ if (section.compare("General", Qt::CaseInsensitive) == 0) {
+ return key;
+ } else {
+ return section + "/" + key;
+ }
+ }
+}
+
+void removeImpl(
+ QSettings& settings, const QString& displayName,
+ const QString& section, const QString& key)
+{
+ if (key.isEmpty()) {
+ if (!settings.childGroups().contains(section, Qt::CaseInsensitive)) {
+ // not there
+ return;
+ }
+ } else {
+ if (!settings.contains(settingName(section, key))) {
+ // not there
+ return;
+ }
+ }
+
+ logRemoval(displayName);
+ settings.remove(settingName(section, key));
+}
+
+void remove(QSettings& settings, const QString& section, const QString& key)
+{
+ removeImpl(settings, settingName(section, key), section, key);
+}
+
+void removeSection(QSettings& settings, const QString& section)
+{
+ removeImpl(settings, section, section, "");
+}
+
+
+ScopedGroup::ScopedGroup(QSettings& s, const QString& name)
+ : m_settings(s), m_name(name)
+{
+ m_settings.beginGroup(m_name);
+}
+
+ScopedGroup::~ScopedGroup()
+{
+ m_settings.endGroup();
+}
+
+void ScopedGroup::remove(const QString& key)
+{
+ removeImpl(m_settings, settingName(m_name, key), "", key);
+}
+
+QStringList ScopedGroup::keys() const
+{
+ return m_settings.childKeys();
+}
+
+
+ScopedReadArray::ScopedReadArray(QSettings& s, const QString& section)
+ : m_settings(s), m_count(0)
+{
+ m_count = m_settings.beginReadArray(section);
+}
+
+ScopedReadArray::~ScopedReadArray()
+{
+ m_settings.endArray();
+}
+
+int ScopedReadArray::count() const
+{
+ return m_count;
+}
+
+QStringList ScopedReadArray::keys() const
+{
+ return m_settings.childKeys();
+}
+
+
+ScopedWriteArray::ScopedWriteArray(
+ QSettings& s, const QString& section, std::size_t size)
+ : m_settings(s), m_section(section), m_i(0)
+{
+ m_settings.beginWriteArray(
+ section, size == NoSize ? -1 : static_cast<int>(size));
+}
+
+ScopedWriteArray::~ScopedWriteArray()
+{
+ m_settings.endArray();
+}
+
+void ScopedWriteArray::next()
+{
+ m_settings.setArrayIndex(m_i);
+ ++m_i;
+}
+
+
+QString widgetNameWithTopLevel(const QWidget* widget)
+{
+ QStringList components;
+
+ auto* tl = widget->window();
+
+ if (tl == widget) {
+ // this is a top level widget, such as a dialog
+ components.push_back(widget->objectName());
+ } else {
+ // this is a widget
+ const auto toplevelName = tl->objectName();
+ if (!toplevelName.isEmpty()) {
+ components.push_back(toplevelName);
+ }
+
+ const auto widgetName = widget->objectName();
+ if (!widgetName.isEmpty()) {
+ components.push_back(widgetName);
+ }
+ }
+
+ if (components.isEmpty()) {
+ // can't do much
+ return "unknown_widget";
+ }
+
+ return components.join("_");
+}
+
+QString widgetName(const QMainWindow* w)
+{
+ return w->objectName();
+}
+
+QString widgetName(const QHeaderView* w)
+{
+ return widgetNameWithTopLevel(w->parentWidget());
+}
+
+QString widgetName(const ExpanderWidget* w)
+{
+ return widgetNameWithTopLevel(w->button());
+}
+
+QString widgetName(const QWidget* w)
+{
+ return widgetNameWithTopLevel(w);
+}
+
+QString dockSettingName(const QDockWidget* dock)
+{
+ return "MainWindow_docks_" + dock->objectName() + "_size";
+}
+
+QString indexSettingName(const QWidget* widget)
+{
+ return widgetNameWithTopLevel(widget) + "_index";
+}
+
+QString checkedSettingName(const QAbstractButton* b)
+{
+ return widgetNameWithTopLevel(b) + "_checked";
+}
+
+void warnIfNotCheckable(const QAbstractButton* b)
+{
+ if (!b->isCheckable()) {
+ log::warn(
+ "button '{}' used in the settings as a checkbox or radio button "
+ "but is not checkable", b->objectName());
+ }
+}
+
+
+QString credentialName(const QString& key)
+{
+ return "ModOrganizer2_" + key;
+}
+
+bool deleteWindowsCredential(const QString& key)
+{
+ const auto credName = credentialName(key);
+
+ if (!CredDeleteW(credName.toStdWString().c_str(), CRED_TYPE_GENERIC, 0)) {
+ const auto e = GetLastError();
+
+ // not an error if the key already doesn't exist, and don't log it because
+ // it happens all the time when the settings dialog is closed since it
+ // doesn't check first
+ if (e == ERROR_NOT_FOUND) {
+ return true;
+ }
+
+ log::error(
+ "failed to delete windows credential {}, {}",
+ credName, formatSystemMessage(e));
+ return false;
+ }
+
+ log::debug("deleted windows credential {}", credName);
+
+ return true;
+}
+
+bool addWindowsCredential(const QString& key, const QString& data)
+{
+ const auto credName = credentialName(key);
+
+ const auto wname = credName.toStdWString();
+ const auto wdata = data.toStdWString();
+
+ const auto* blob = reinterpret_cast<const BYTE*>(wdata.data());
+ const auto blobSize = wdata.size() * sizeof(decltype(wdata)::value_type);
+
+ CREDENTIALW cred = {};
+ cred.Flags = 0;
+ cred.Type = CRED_TYPE_GENERIC;
+ cred.TargetName = const_cast<wchar_t*>(wname.c_str());
+ cred.CredentialBlob = const_cast<BYTE*>(blob);
+ cred.CredentialBlobSize = static_cast<DWORD>(blobSize);
+ cred.Persist = CRED_PERSIST_LOCAL_MACHINE;
+
+ if (!CredWriteW(&cred, 0)) {
+ const auto e = GetLastError();
+
+ log::error(
+ "failed to delete windows credential {}, {}",
+ credName, formatSystemMessage(e));
+
+ return false;
+ }
+
+ log::debug("set windows credential {}", credName);
+
+ return true;
+}
+
+struct CredentialFreer
+{
+ void operator()(CREDENTIALW* c)
+ {
+ if (c) {
+ CredFree(c);
+ }
+ }
+};
+
+using CredentialPtr = std::unique_ptr<CREDENTIALW, CredentialFreer>;
+
+QString getWindowsCredential(const QString& key)
+{
+ const QString credName = credentialName(key);
+
+ CREDENTIALW* rawCreds = nullptr;
+
+ const auto ret = CredReadW(
+ credName.toStdWString().c_str(), CRED_TYPE_GENERIC, 0, &rawCreds);
+
+ CredentialPtr creds(rawCreds);
+
+ if (!ret) {
+ const auto e = GetLastError();
+
+ if (e != ERROR_NOT_FOUND) {
+ log::error(
+ "failed to retrieve windows credential {}: {}",
+ credName, formatSystemMessage(e));
+ }
+
+ return {};
+ }
+
+ QString value;
+ if (creds->CredentialBlob) {
+ value = QString::fromWCharArray(
+ reinterpret_cast<const wchar_t*>(creds->CredentialBlob),
+ creds->CredentialBlobSize / sizeof(wchar_t));
+ }
+
+ return value;
+}
+
+bool setWindowsCredential(const QString& key, const QString& data)
+{
+ if (data.isEmpty()) {
+ return deleteWindowsCredential(key);
+ } else {
+ return addWindowsCredential(key, data);
+ }
+}
diff --git a/src/settingsutilities.h b/src/settingsutilities.h
new file mode 100644
index 00000000..ac6aeb29
--- /dev/null
+++ b/src/settingsutilities.h
@@ -0,0 +1,276 @@
+#ifndef SETTINGSUTILITIES_H
+#define SETTINGSUTILITIES_H
+
+#include <log.h>
+
+namespace MOBase {
+ class ExpanderWidget;
+}
+
+template <class T, class=void>
+struct ValueConverter
+{
+ static const T& convert(const T& t)
+ {
+ return t;
+ }
+};
+
+template <class T>
+struct ValueConverter<T, std::enable_if_t<std::is_enum_v<T>>>
+{
+ static QString convert(const T& t)
+ {
+ return QString("%1").arg(static_cast<std::underlying_type_t<T>>(t));
+ }
+};
+
+
+bool shouldLogSetting(const QString& displayName);
+
+template <class T>
+void logChange(
+ const QString& displayName, std::optional<T> oldValue, const T& newValue)
+{
+ if (!shouldLogSetting(displayName)) {
+ return;
+ }
+
+ using VC = ValueConverter<T>;
+
+ if (oldValue) {
+ MOBase::log::debug(
+ "setting '{}' changed from '{}' to '{}'",
+ displayName, VC::convert(*oldValue), VC::convert(newValue));
+ } else {
+ MOBase::log::debug(
+ "setting '{}' set to '{}'",
+ displayName, VC::convert(newValue));
+ }
+}
+
+void logRemoval(const QString& name);
+
+
+QString settingName(const QString& section, const QString& key);
+
+template <class T>
+void setImpl(
+ QSettings& settings, const QString& displayName,
+ const QString& section, const QString& key, const T& value)
+{
+ const auto current = getOptional<T>(settings, section, key);
+
+ if (current && *current == value) {
+ // no change
+ return;
+ }
+
+ const auto name = settingName(section, key);
+
+ logChange(displayName, current, value);
+
+ if constexpr (std::is_enum_v<T>) {
+ settings.setValue(
+ name, static_cast<std::underlying_type_t<T>>(value));
+ } else {
+ settings.setValue(name, value);
+ }
+}
+
+void removeImpl(
+ QSettings& settings, const QString& displayName,
+ const QString& section, const QString& key);
+
+
+template <class T>
+std::optional<T> getOptional(
+ const QSettings& settings,
+ const QString& section, const QString& key, std::optional<T> def={})
+{
+ if (settings.contains(settingName(section, key))) {
+ const auto v = settings.value(settingName(section, key));
+
+ if constexpr (std::is_enum_v<T>) {
+ return static_cast<T>(v.value<std::underlying_type_t<T>>());
+ } else {
+ return v.value<T>();
+ }
+ }
+
+ return def;
+}
+
+template <class T>
+T get(
+ const QSettings& settings,
+ const QString& section, const QString& key, T def)
+{
+ if (auto v=getOptional<T>(settings, section, key)) {
+ return *v;
+ } else {
+ return def;
+ }
+}
+
+template <class T>
+void set(
+ QSettings& settings,
+ const QString& section, const QString& key, const T& value)
+{
+ setImpl(settings, settingName(section, key), section, key, value);
+}
+
+void remove(QSettings& settings, const QString& section, const QString& key);
+void removeSection(QSettings& settings, const QString& section);
+
+
+class ScopedGroup
+{
+public:
+ ScopedGroup(QSettings& s, const QString& name);
+ ~ScopedGroup();
+
+ ScopedGroup(const ScopedGroup&) = delete;
+ ScopedGroup& operator=(const ScopedGroup&) = delete;
+
+ template <class T>
+ void set(const QString& key, const T& value)
+ {
+ setImpl(m_settings, settingName(m_name, key), "", key, value);
+ }
+
+ void remove(const QString& key);
+
+ QStringList keys() const;
+
+ template <class F>
+ void for_each(F&& f) const
+ {
+ for (const QString& key : keys()) {
+ f(key);
+ }
+ }
+
+ template <class T>
+ std::optional<T> getOptional(const QString& key, std::optional<T> def={}) const
+ {
+ return ::getOptional<T>(m_settings, "", key, def);
+ }
+
+ template <class T>
+ T get(const QString& key, T def={}) const
+ {
+ return ::get<T>(m_settings, "", key, def);
+ }
+
+private:
+ QSettings& m_settings;
+ QString m_name;
+};
+
+
+class ScopedReadArray
+{
+public:
+ ScopedReadArray(QSettings& s, const QString& section);
+ ~ScopedReadArray();
+
+ ScopedReadArray(const ScopedReadArray&) = delete;
+ ScopedReadArray& operator=(const ScopedReadArray&) = delete;
+
+ template <class F>
+ void for_each(F&& f) const
+ {
+ for (int i=0; i<count(); ++i) {
+ m_settings.setArrayIndex(i);
+ f();
+ }
+ }
+
+ template <class T>
+ std::optional<T> getOptional(const QString& key, std::optional<T> def={}) const
+ {
+ return ::getOptional<T>(m_settings, "", key, def);
+ }
+
+ template <class T>
+ T get(const QString& key, T def={}) const
+ {
+ return ::get<T>(m_settings, "", key, def);
+ }
+
+ int count() const;
+ QStringList keys() const;
+
+private:
+ QSettings& m_settings;
+ int m_count;
+};
+
+
+class ScopedWriteArray
+{
+public:
+ static const auto NoSize = std::numeric_limits<std::size_t>::max();
+
+ ScopedWriteArray(QSettings& s, const QString& section, std::size_t size=NoSize);
+ ~ScopedWriteArray();
+
+ ScopedWriteArray(const ScopedWriteArray&) = delete;
+ ScopedWriteArray& operator=(const ScopedWriteArray&) = delete;
+
+ void next();
+
+ template <class T>
+ void set(const QString& key, const T& value)
+ {
+ const auto displayName = QString("%1/%2\\%3")
+ .arg(m_section)
+ .arg(m_i)
+ .arg(key);
+
+ setImpl(m_settings, displayName, "", key, value);
+ }
+
+private:
+ QSettings& m_settings;
+ QString m_section;
+ int m_i;
+};
+
+
+QString widgetNameWithTopLevel(const QWidget* widget);
+QString widgetName(const QMainWindow* w);
+QString widgetName(const QHeaderView* w);
+QString widgetName(const MOBase::ExpanderWidget* w);
+QString widgetName(const QWidget* w);
+
+template <class Widget>
+QString geoSettingName(const Widget* widget)
+{
+ return widgetName(widget) + "_geometry";
+}
+
+template <class Widget>
+QString stateSettingName(const Widget* widget)
+{
+ return widgetName(widget) + "_state";
+}
+
+template <class Widget>
+QString visibilitySettingName(const Widget* widget)
+{
+ return widgetName(widget) + "_visibility";
+}
+
+QString dockSettingName(const QDockWidget* dock);
+QString indexSettingName(const QWidget* widget);
+QString checkedSettingName(const QAbstractButton* b);
+
+void warnIfNotCheckable(const QAbstractButton* b);
+
+bool setWindowsCredential(const QString& key, const QString& data);
+QString getWindowsCredential(const QString& key);
+
+#endif // SETTINGSUTILITIES_H
diff --git a/src/shared/appconfig.inc b/src/shared/appconfig.inc
index e572a32b..709c845d 100644
--- a/src/shared/appconfig.inc
+++ b/src/shared/appconfig.inc
@@ -17,6 +17,7 @@ APPPARAM(std::wstring, proxyDLLOrig, L"steam_api_orig.dll") // needs to be ident
APPPARAM(std::wstring, proxyDLLSource, L"proxy.dll")
APPPARAM(std::wstring, vfs32DLLName, L"usvfs_x86.dll")
APPPARAM(std::wstring, vfs64DLLName, L"usvfs_x64.dll")
+APPPARAM(std::wstring, portableLockFileName, L"portable.txt")
APPPARAM(const wchar_t*, localSavePlaceholder, L"__MOProfileSave__\\")
APPPARAM(std::wstring, firstStepsTutorial, L"tutorial_firststeps_main.js")
diff --git a/src/shared/directoryentry.cpp b/src/shared/directoryentry.cpp
index bde515a9..00bf319e 100644
--- a/src/shared/directoryentry.cpp
+++ b/src/shared/directoryentry.cpp
@@ -21,6 +21,7 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#include "windows_error.h"
#include "leaktrace.h"
#include "error_report.h"
+#include <log.h>
#include <bsatk.h>
#include <boost/bind.hpp>
#include <boost/scoped_array.hpp>
@@ -31,10 +32,13 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#include <algorithm>
#include <map>
#include <atomic>
+#include <QObject>
namespace MOShared {
+namespace log = MOBase::log;
+
static const int MAXPATH_UNICODE = 32767;
class OriginConnection {
@@ -80,7 +84,7 @@ public:
return m_Origins[iter->second];
} else {
std::ostringstream stream;
- stream << "invalid origin name: " << ToString(name, false);
+ stream << QObject::tr("invalid origin name: ").toStdString() << ToString(name, true);
throw std::runtime_error(stream.str());
}
}
@@ -103,7 +107,7 @@ public:
m_OriginsNameMap.erase(iter);
m_OriginsNameMap[newName] = idx;
} else {
- log("failed to change name lookup from %ls to %ls", oldName.c_str(), newName.c_str());
+ log::error(QObject::tr("failed to change name lookup from {} to {}").toStdString(), oldName, newName);
}
}
@@ -517,7 +521,7 @@ void DirectoryEntry::addFromBSA(const std::wstring &originName, std::wstring &di
WIN32_FILE_ATTRIBUTE_DATA fileData;
if (::GetFileAttributesExW(fileName.c_str(), GetFileExInfoStandard, &fileData) == 0) {
- throw windows_error("failed to determine file time");
+ throw windows_error(QObject::tr("failed to determine file time").toStdString());
}
FILETIME now;
::GetSystemTimeAsFileTime(&now);
@@ -539,7 +543,7 @@ void DirectoryEntry::addFromBSA(const std::wstring &originName, std::wstring &di
BSA::EErrorCode res = archive.read(ToString(fileName, false).c_str(), false);
if ((res != BSA::ERROR_NONE) && (res != BSA::ERROR_INVALIDHASHES)) {
std::ostringstream stream;
- stream << "invalid bsa file: " << ToString(fileName, false) << " errorcode " << res << " - " << ::GetLastError();
+ stream << QObject::tr("invalid bsa file: ").toStdString() << ToString(fileName, false) << " errorcode " << res << " - " << ::GetLastError();
throw std::runtime_error(stream.str());
}
@@ -714,14 +718,14 @@ void DirectoryEntry::removeFile(FileEntry::Index index)
if (iter != m_Files.end()) {
m_Files.erase(iter);
} else {
- log("file \"%ls\" not in directory \"%ls\"",
- m_FileRegister->getFile(index)->getName().c_str(),
- this->getName().c_str());
+ log::error(
+ QObject::tr("file \"{}\" not in directory \"{}\"").toStdString(),
+ m_FileRegister->getFile(index)->getName(), this->getName());
}
} else {
- log("file \"%ls\" not in directory \"%ls\", directory empty",
- m_FileRegister->getFile(index)->getName().c_str(),
- this->getName().c_str());
+ log::error(
+ QObject::tr("file \"{}\" not in directory \"{}\", directory empty").toStdString(),
+ m_FileRegister->getFile(index)->getName(), this->getName());
}
}
@@ -844,7 +848,7 @@ const FileEntry::Ptr DirectoryEntry::searchFile(const std::wstring &path, const
DirectoryEntry *temp = findSubDirectory(pathComponent);
if (temp != nullptr) {
if (len >= path.size()) {
- log("unexpected end of path");
+ log::error(QObject::tr("unexpected end of path").toStdString());
return FileEntry::Ptr();
}
return temp->searchFile(path.substr(len + 1), directory);
@@ -988,7 +992,7 @@ bool FileRegister::removeFile(FileEntry::Index index)
m_Files.erase(index);
return true;
} else {
- log("invalid file index for remove: %lu", index);
+ log::error(QObject::tr("invalid file index for remove: {}").toStdString(), index);
return false;
}
}
@@ -1002,7 +1006,7 @@ void FileRegister::removeOrigin(FileEntry::Index index, int originID)
m_Files.erase(iter);
}
} else {
- log("invalid file index for remove (for origin): %lu", index);
+ log::error(QObject::tr("invalid file index for remove (for origin): {}").toStdString(), index);
}
}
diff --git a/src/shared/error_report.cpp b/src/shared/error_report.cpp
index 6d091630..4185b544 100644
--- a/src/shared/error_report.cpp
+++ b/src/shared/error_report.cpp
@@ -23,7 +23,6 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
namespace MOShared {
-
void reportError(LPCSTR format, ...)
{
char buffer[1025];
@@ -52,48 +51,4 @@ void reportError(LPCWSTR format, ...)
MessageBoxW(nullptr, buffer, L"Error", MB_OK | MB_ICONERROR);
}
-
-std::string getCurrentErrorStringA()
-{
- LPSTR buffer = nullptr;
-
- DWORD errorCode = ::GetLastError();
-
- if (FormatMessageA(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM,
- nullptr, errorCode, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPSTR)&buffer, 0, nullptr) == 0) {
- ::SetLastError(errorCode);
- return std::string();
- } else {
- LPSTR lastChar = buffer + strlen(buffer) - 2;
- *lastChar = '\0';
-
- std::string result(buffer);
-
- LocalFree(buffer);
- ::SetLastError(errorCode);
- return result;
- }
-}
-
-std::wstring getCurrentErrorStringW()
-{
- LPWSTR buffer = nullptr;
-
- DWORD errorCode = ::GetLastError();
-
- if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM,
- nullptr, errorCode, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPWSTR)&buffer, 0, nullptr) == 0) {
- ::SetLastError(errorCode);
- return std::wstring();
- } else {
- LPWSTR lastChar = buffer + wcslen(buffer) - 2;
- *lastChar = '\0';
-
- std::wstring result(buffer);
-
- LocalFree(buffer);
- ::SetLastError(errorCode);
- return result;
- }
-}
} // namespace MOShared
diff --git a/src/shared/error_report.h b/src/shared/error_report.h
index c09ad75b..17b25645 100644
--- a/src/shared/error_report.h
+++ b/src/shared/error_report.h
@@ -24,28 +24,9 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#include <Windows.h>
#include <string>
-namespace std {
-#ifdef UNICODE
-typedef wstring tstring;
-#else
-typedef string tstring;
-#endif
-}
-
-extern void log(const char* format, ...);
-
namespace MOShared {
void reportError(LPCSTR format, ...);
void reportError(LPCWSTR format, ...);
-std::string getCurrentErrorStringA();
-std::wstring getCurrentErrorStringW();
-
-#ifdef UNICODE
-#define getCurrentErrorString getCurrentErrorStringW
-#else
-#define getCurrentErrorString getCurrentErrorStringA
-#endif
-
} // namespace MOShared
diff --git a/src/shared/util.cpp b/src/shared/util.cpp
index 17df3b92..baceddeb 100644
--- a/src/shared/util.cpp
+++ b/src/shared/util.cpp
@@ -19,34 +19,12 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#include "util.h"
#include "windows_error.h"
-#include "error_report.h"
-#include "executableslist.h"
-#include "instancemanager.h"
-#include <utility.h>
-
-#include <sstream>
-#include <locale>
-#include <algorithm>
-#include <set>
-#include <filesystem>
-
-#include <DbgHelp.h>
-#include <boost/scoped_array.hpp>
-#include <QApplication>
-
-#include <comdef.h>
-#include <Wbemidl.h>
-#include <wscapi.h>
-#include <netfw.h>
-
-#pragma comment(lib, "Wbemuuid.lib")
-
-using MOBase::formatSystemMessage;
-using MOBase::formatSystemMessageQ;
-namespace fs = std::filesystem;
-
-namespace MOShared {
+#include "mainwindow.h"
+#include <usvfs.h>
+#include <usvfs_version.h>
+namespace MOShared
+{
bool FileExists(const std::string &filename)
{
@@ -268,1808 +246,103 @@ MOBase::VersionInfo createVersionInfo()
}
}
-
-namespace env
-{
-
-struct HandleCloser
-{
- using pointer = HANDLE;
-
- void operator()(HANDLE h)
- {
- if (h != INVALID_HANDLE_VALUE) {
- ::CloseHandle(h);
- }
- }
-};
-
-using HandlePtr = std::unique_ptr<HANDLE, HandleCloser>;
-
-
-struct LibraryFreer
-{
- using pointer = HINSTANCE;
-
- void operator()(HINSTANCE h)
- {
- if (h != 0) {
- ::FreeLibrary(h);
- }
- }
-};
-
-struct COMReleaser
-{
- void operator()(IUnknown* p)
- {
- if (p) {
- p->Release();
- }
- }
-};
-
-
-template <class T>
-using COMPtr = std::unique_ptr<T, COMReleaser>;
-
-
-class ShellLinkException
-{
-public:
- ShellLinkException(QString s)
- : m_what(std::move(s))
- {
- }
-
- const QString& what() const
- {
- return m_what;
- }
-
-private:
- QString m_what;
-};
-
-// just a wrapper around IShellLink operations that throws ShellLinkException
-// on errors
-//
-class ShellLinkWrapper
-{
-public:
- ShellLinkWrapper()
- {
- m_link = createShellLink();
- m_file = createPersistFile();
- }
-
- void setPath(const QString& s)
- {
- if (s.isEmpty()) {
- throw ShellLinkException("path cannot be empty");
- }
-
- const auto r = m_link->SetPath(s.toStdWString().c_str());
- throwOnFail(r, QString("failed to set target path '%1'").arg(s));
- }
-
- void setArguments(const QString& s)
- {
- const auto r = m_link->SetArguments(s.toStdWString().c_str());
- throwOnFail(r, QString("failed to set arguments '%1'").arg(s));
- }
-
- void setDescription(const QString& s)
- {
- if (s.isEmpty()) {
- return;
- }
-
- const auto r = m_link->SetDescription(s.toStdWString().c_str());
- throwOnFail(r, QString("failed to set description '%1'").arg(s));
- }
-
- void setIcon(const QString& file, int i)
- {
- if (file.isEmpty()) {
- return;
- }
-
- const auto r = m_link->SetIconLocation(file.toStdWString().c_str(), i);
- throwOnFail(r, QString("failed to set icon '%1' @ %2").arg(file).arg(i));
- }
-
- void setWorkingDirectory(const QString& s)
- {
- if (s.isEmpty()) {
- return;
- }
-
- const auto r = m_link->SetWorkingDirectory(s.toStdWString().c_str());
- throwOnFail(r, QString("failed to set working directory '%1'").arg(s));
- }
-
- void save(const QString& path)
- {
- const auto r = m_file->Save(path.toStdWString().c_str(), TRUE);
- throwOnFail(r, QString("failed to save link '%1'").arg(path));
- }
-
-private:
- COMPtr<IShellLink> m_link;
- COMPtr<IPersistFile> m_file;
-
- void throwOnFail(HRESULT r, const QString& s)
- {
- if (FAILED(r)) {
- throw ShellLinkException(QString("%1, %2")
- .arg(s)
- .arg(formatSystemMessageQ(r)));
- }
- }
-
- COMPtr<IShellLink> createShellLink()
- {
- void* link = nullptr;
-
- const auto r = CoCreateInstance(
- CLSID_ShellLink, nullptr, CLSCTX_INPROC_SERVER,
- IID_IShellLink, &link);
-
- throwOnFail(r, "failed to create IShellLink instance");
-
- if (!link) {
- throw ShellLinkException("creating IShellLink worked, pointer is null");
- }
-
- return COMPtr<IShellLink>(static_cast<IShellLink*>(link));
- }
-
- COMPtr<IPersistFile> createPersistFile()
- {
- void* file = nullptr;
-
- const auto r = m_link->QueryInterface(IID_IPersistFile, &file);
- throwOnFail(r, "failed to get IPersistFile interface");
-
- if (!file) {
- throw ShellLinkException("querying IPersistFile worked, pointer is null");
- }
-
- return COMPtr<IPersistFile>(static_cast<IPersistFile*>(file));
- }
-};
-
-
-Shortcut::Shortcut()
- : m_iconIndex(0)
-{
-}
-
-Shortcut::Shortcut(const Executable& exe)
- : Shortcut()
-{
- m_name = exe.title();
- m_target = QFileInfo(qApp->applicationFilePath()).absoluteFilePath();
-
- m_arguments = QString("\"moshortcut://%1:%2\"")
- .arg(InstanceManager::instance().currentInstance())
- .arg(exe.title());
-
- m_description = QString("Run %1 with ModOrganizer").arg(exe.title());
-
- if (exe.usesOwnIcon()) {
- m_icon = exe.binaryInfo().absoluteFilePath();
- }
-
- m_workingDirectory = qApp->applicationDirPath();
-}
-
-Shortcut& Shortcut::name(const QString& s)
-{
- m_name = s;
- return *this;
-}
-
-Shortcut& Shortcut::target(const QString& s)
-{
- m_target = s;
- return *this;
-}
-
-Shortcut& Shortcut::arguments(const QString& s)
-{
- m_arguments = s;
- return *this;
-}
-
-Shortcut& Shortcut::description(const QString& s)
-{
- m_description = s;
- return *this;
-}
-
-Shortcut& Shortcut::icon(const QString& s, int index)
-{
- m_icon = s;
- m_iconIndex = index;
- return *this;
-}
-
-Shortcut& Shortcut::workingDirectory(const QString& s)
-{
- m_workingDirectory = s;
- return *this;
-}
-
-bool Shortcut::exists(Locations loc) const
-{
- const auto path = shortcutPath(loc);
- if (path.isEmpty()) {
- return false;
- }
-
- return QFileInfo(path).exists();
-}
-
-bool Shortcut::toggle(Locations loc)
-{
- if (exists(loc)) {
- return remove(loc);
- } else {
- return add(loc);
- }
-}
-
-bool Shortcut::add(Locations loc)
-{
- debug()
- << "adding shortcut to " << toString(loc) << ":\n"
- << " . name: '" << m_name << "'\n"
- << " . target: '" << m_target << "'\n"
- << " . arguments: '" << m_arguments << "'\n"
- << " . description: '" << m_description << "'\n"
- << " . icon: '" << m_icon << "' @ " << m_iconIndex << "\n"
- << " . working directory: '" << m_workingDirectory << "'";
-
- if (m_target.isEmpty()) {
- critical() << "target is empty";
- return false;
- }
-
- const auto path = shortcutPath(loc);
- if (path.isEmpty()) {
- return false;
- }
-
- debug() << "shorcut file will be saved at '" << path << "'";
-
- try
- {
- ShellLinkWrapper link;
-
- link.setPath(m_target);
- link.setArguments(m_arguments);
- link.setDescription(m_description);
- link.setIcon(m_icon, m_iconIndex);
- link.setWorkingDirectory(m_workingDirectory);
-
- link.save(path);
-
- return true;
- }
- catch(ShellLinkException& e)
- {
- critical() << e.what() << "\nshortcut file was not saved";
- }
-
- return false;
-}
-
-bool Shortcut::remove(Locations loc)
-{
- debug() << "removing shortcut for '" << m_name << "' from " << toString(loc);
-
- const auto path = shortcutPath(loc);
- if (path.isEmpty()) {
- return false;
- }
-
- debug() << "path to shortcut file is '" << path << "'";
-
- if (!QFile::exists(path)) {
- critical() << "can't remove '" << path << "', file not found";
- return false;
- }
-
- if (!MOBase::shellDelete({path})) {
- const auto e = ::GetLastError();
-
- critical()
- << "failed to remove '" << path << "', "
- << formatSystemMessageQ(e);
-
- return false;
- }
-
- return true;
-}
-
-QString Shortcut::shortcutPath(Locations loc) const
-{
- const auto dir = shortcutDirectory(loc);
- if (dir.isEmpty()) {
- return {};
- }
-
- const auto file = shortcutFilename();
- if (file.isEmpty()) {
- return {};
- }
-
- return dir + QDir::separator() + file;
-}
-
-QString Shortcut::shortcutDirectory(Locations loc) const
-{
- QString dir;
-
- try
- {
- switch (loc)
- {
- case Desktop:
- dir = MOBase::getDesktopDirectory();
- break;
-
- case StartMenu:
- dir = MOBase::getStartMenuDirectory();
- break;
-
- case None:
- default:
- critical() << "bad location " << loc;
- break;
- }
- }
- catch(std::exception&)
- {
- }
-
- return QDir::toNativeSeparators(dir);
-}
-
-QString Shortcut::shortcutFilename() const
-{
- if (m_name.isEmpty()) {
- critical() << "name is empty";
- return {};
- }
-
- return m_name + ".lnk";
-}
-
-QDebug Shortcut::debug() const
-{
- return qDebug().noquote().nospace() << "system shortcut: ";
-}
-
-QDebug Shortcut::critical() const
-{
- return qCritical().noquote().nospace() << "system shortcut: ";
-}
-
-
-QString toString(Shortcut::Locations loc)
-{
- switch (loc)
- {
- case Shortcut::None:
- return "none";
-
- case Shortcut::Desktop:
- return "desktop";
-
- case Shortcut::StartMenu:
- return "start menu";
-
- default:
- return QString("? (%1)").arg(static_cast<int>(loc));
- }
-}
-
-
-
-class WMI
-{
-public:
- class failed {};
-
- WMI(const std::string& ns)
- {
- try
- {
- createLocator();
- createService(ns);
- setSecurity();
- }
- catch(failed&)
- {
- }
- }
-
- template <class F>
- void query(const std::string& q, F&& f)
- {
- if (!m_locator || !m_service) {
- return;
- }
-
- auto enumerator = getEnumerator(q);
- if (!enumerator) {
- return;
- }
-
- for (;;)
- {
- COMPtr<IWbemClassObject> object;
-
- {
- IWbemClassObject* rawObject = nullptr;
- ULONG count = 0;
- auto ret = enumerator->Next(WBEM_INFINITE, 1, &rawObject, &count);
-
- if (count == 0 || !rawObject) {
- break;
- }
-
- if (FAILED(ret)) {
- qCritical()
- << "enumerator->next() failed, " << formatSystemMessageQ(ret);
- break;
- }
-
- object.reset(rawObject);
- }
-
- f(object.get());
- }
- }
-
-private:
- COMPtr<IWbemLocator> m_locator;
- COMPtr<IWbemServices> m_service;
-
- void createLocator()
- {
- void* rawLocator = nullptr;
-
- const auto ret = CoCreateInstance(
- CLSID_WbemLocator, nullptr, CLSCTX_INPROC_SERVER,
- IID_IWbemLocator, &rawLocator);
-
- if (FAILED(ret) || !rawLocator) {
- qCritical()
- << "CoCreateInstance for WbemLocator failed, "
- << formatSystemMessageQ(ret);
-
- throw failed();
- }
-
- m_locator.reset(static_cast<IWbemLocator*>(rawLocator));
- }
-
- void createService(const std::string& ns)
- {
- IWbemServices* rawService = nullptr;
-
- const auto res = m_locator->ConnectServer(
- _bstr_t(ns.c_str()),
- nullptr, nullptr, nullptr, 0, nullptr, nullptr,
- &rawService);
-
- if (FAILED(res) || !rawService) {
- qCritical()
- << "locator->ConnectServer() failed for namespace "
- << "'" << QString::fromStdString(ns) << "', "
- << formatSystemMessageQ(res);
-
- throw failed();
- }
-
- m_service.reset(rawService);
- }
-
- void setSecurity()
- {
- auto ret = CoSetProxyBlanket(
- m_service.get(), RPC_C_AUTHN_WINNT, RPC_C_AUTHZ_NONE, nullptr,
- RPC_C_AUTHN_LEVEL_CALL, RPC_C_IMP_LEVEL_IMPERSONATE, 0, EOAC_NONE);
-
- if (FAILED(ret))
- {
- qCritical()
- << "CoSetProxyBlanket() failed, " << formatSystemMessageQ(ret);
-
- throw failed();
- }
- }
-
- COMPtr<IEnumWbemClassObject> getEnumerator(
- const std::string& query)
- {
- IEnumWbemClassObject* rawEnumerator = NULL;
-
- auto ret = m_service->ExecQuery(
- bstr_t("WQL"),
- bstr_t(query.c_str()),
- WBEM_FLAG_FORWARD_ONLY | WBEM_FLAG_RETURN_IMMEDIATELY,
- NULL,
- &rawEnumerator);
-
- if (FAILED(ret) || !rawEnumerator)
- {
- qCritical()
- << "query '" << QString::fromStdString(query) << "' failed, "
- << formatSystemMessageQ(ret);
-
- return {};
- }
-
- return COMPtr<IEnumWbemClassObject>(rawEnumerator);
- }
-};
-
-
-Environment::Environment()
-{
- m_modules = getLoadedModules();
- m_security = getSecurityProducts();
-}
-
-const std::vector<Module>& Environment::loadedModules()
-{
- return m_modules;
-}
-
-const WindowsInfo& Environment::windowsInfo() const
-{
- return m_windows;
-}
-
-const std::vector<SecurityProduct>& Environment::securityProducts() const
-{
- return m_security;
-}
-
-std::vector<Module> Environment::getLoadedModules() const
-{
- HandlePtr snapshot(CreateToolhelp32Snapshot(
- TH32CS_SNAPMODULE32 | TH32CS_SNAPMODULE, GetCurrentProcessId()));
-
- if (snapshot.get() == INVALID_HANDLE_VALUE)
- {
- const auto e = GetLastError();
-
- qCritical().nospace().noquote()
- << "CreateToolhelp32Snapshot() failed, "
- << formatSystemMessageQ(e);
-
- return {};
- }
-
- MODULEENTRY32 me = {};
- me.dwSize = sizeof(me);
-
- // first module, this shouldn't fail because there's at least the executable
- if (!Module32First(snapshot.get(), &me))
- {
- const auto e = GetLastError();
-
- qCritical().nospace().noquote()
- << "Module32First() failed, " << formatSystemMessageQ(e);
-
- return {};
- }
-
- std::vector<Module> v;
-
- for (;;)
- {
- const auto path = QString::fromWCharArray(me.szExePath);
- if (!path.isEmpty()) {
- v.push_back(Module(path, me.modBaseSize));
- }
-
- // next module
- if (!Module32Next(snapshot.get(), &me)) {
- const auto e = GetLastError();
-
- // no more modules is not an error
- if (e != ERROR_NO_MORE_FILES) {
- qCritical().nospace().noquote()
- << "Module32Next() failed, " << formatSystemMessageQ(e);
- }
-
- break;
- }
- }
-
- // sorting by display name
- std::sort(v.begin(), v.end(), [](auto&& a, auto&& b) {
- return (a.displayPath().compare(b.displayPath(), Qt::CaseInsensitive) < 0);
- });
-
- return v;
-}
-
-std::vector<SecurityProduct> Environment::getSecurityProducts() const
-{
- std::vector<SecurityProduct> v;
-
- {
- auto fromWMI = getSecurityProductsFromWMI();
- v.insert(
- v.end(),
- std::make_move_iterator(fromWMI.begin()),
- std::make_move_iterator(fromWMI.end()));
- }
-
- if (auto p=getWindowsFirewall()) {
- v.push_back(std::move(*p));
- }
-
- return v;
-}
-
-std::vector<SecurityProduct> Environment::getSecurityProductsFromWMI() const
-{
- // some products may be present in multiple queries, such as a product marked
- // as both antivirus and antispyware, but they'll have the same GUID, so use
- // that to avoid duplicating entries
- std::map<QUuid, SecurityProduct> map;
-
- auto handleProduct = [&](auto* o) {
- VARIANT prop;
-
- // display name
- auto ret = o->Get(L"displayName", 0, &prop, 0, 0);
- if (FAILED(ret)) {
- qCritical()
- << "failed to get displayName, "
- << formatSystemMessageQ(ret);
-
- return;
- }
-
- if (prop.vt != VT_BSTR) {
- qCritical() << "displayName is a " << prop.vt << ", not a bstr";
- return;
- }
-
- const std::wstring name = prop.bstrVal;
- VariantClear(&prop);
-
- // product state
- ret = o->Get(L"productState", 0, &prop, 0, 0);
- if (FAILED(ret)) {
- qCritical()
- << "failed to get productState, "
- << formatSystemMessageQ(ret);
-
- return;
- }
-
- if (prop.vt != VT_UI4 && prop.vt != VT_I4) {
- qCritical() << "productState is a " << prop.vt << ", is not a VT_UI4";
- return;
- }
-
- DWORD state = 0;
- if (prop.vt == VT_I4) {
- state = prop.lVal;
- } else {
- state = prop.ulVal;
- }
-
- VariantClear(&prop);
-
- // guid
- ret = o->Get(L"instanceGuid", 0, &prop, 0, 0);
- if (FAILED(ret)) {
- qCritical()
- << "failed to get instanceGuid, "
- << formatSystemMessageQ(ret);
-
- return;
- }
-
- if (prop.vt != VT_BSTR) {
- qCritical() << "instanceGuid is a " << prop.vt << ", is not a bstr";
- return;
- }
-
- const QUuid guid(QString::fromWCharArray(prop.bstrVal));
- VariantClear(&prop);
-
- const auto provider = static_cast<int>((state >> 16) & 0xff);
- const auto scanner = (state >> 8) & 0xff;
- const auto definitions = state & 0xff;
-
- const bool active = ((scanner & 0x10) != 0);
- const bool upToDate = (definitions == 0);
-
- map.insert({
- guid,
- {QString::fromStdWString(name), provider, active, upToDate}});
- };
-
- {
- WMI wmi("root\\SecurityCenter2");
- wmi.query("select * from AntivirusProduct", handleProduct);
- wmi.query("select * from FirewallProduct", handleProduct);
- wmi.query("select * from AntiSpywareProduct", handleProduct);
- }
-
- {
- WMI wmi("root\\SecurityCenter");
- wmi.query("select * from AntivirusProduct", handleProduct);
- wmi.query("select * from FirewallProduct", handleProduct);
- wmi.query("select * from AntiSpywareProduct", handleProduct);
- }
-
- std::vector<SecurityProduct> v;
-
- for (auto&& p : map) {
- v.push_back(p.second);
- }
-
- return v;
-}
-
-std::optional<SecurityProduct> Environment::getWindowsFirewall() const
-{
- HRESULT hr = 0;
-
- COMPtr<INetFwPolicy2> policy;
-
- {
- void* rawPolicy = nullptr;
-
- hr = CoCreateInstance(
- __uuidof(NetFwPolicy2), nullptr, CLSCTX_INPROC_SERVER,
- __uuidof(INetFwPolicy2), &rawPolicy);
-
- if (FAILED(hr) || !rawPolicy) {
- qCritical()
- << "CoCreateInstance for NetFwPolicy2 failed, "
- << formatSystemMessageQ(hr);
-
- return {};
- }
-
- policy.reset(static_cast<INetFwPolicy2*>(rawPolicy));
- }
-
- VARIANT_BOOL enabledVariant;
-
- if (policy) {
- hr = policy->get_FirewallEnabled(NET_FW_PROFILE2_PUBLIC, &enabledVariant);
- if (FAILED(hr))
- {
- qCritical()
- << "get_FirewallEnabled failed, "
- << formatSystemMessageQ(hr);
-
- return {};
- }
- }
-
- const auto enabled = (enabledVariant != VARIANT_FALSE);
- if (!enabled) {
- return {};
- }
-
- return SecurityProduct(
- "Windows Firewall", WSC_SECURITY_PROVIDER_FIREWALL, true, true);
-}
-
-
-Module::Module(QString path, std::size_t fileSize)
- : m_path(std::move(path)), m_fileSize(fileSize)
-{
- const auto fi = getFileInfo();
-
- m_version = getVersion(fi.ffi);
- m_timestamp = getTimestamp(fi.ffi);
- m_versionString = fi.fileDescription;
- m_md5 = getMD5();
-}
-
-const QString& Module::path() const
-{
- return m_path;
-}
-
-QString Module::displayPath() const
-{
- return QDir::fromNativeSeparators(m_path.toLower());
-}
-
-std::size_t Module::fileSize() const
-{
- return m_fileSize;
-}
-
-const QString& Module::version() const
-{
- return m_version;
-}
-
-const QString& Module::versionString() const
-{
- return m_versionString;
-}
-
-const QDateTime& Module::timestamp() const
-{
- return m_timestamp;
-}
-
-const QString& Module::md5() const
-{
- return m_md5;
-}
-
-QString Module::timestampString() const
-{
- if (!m_timestamp.isValid()) {
- return "(no timestamp)";
- }
-
- return m_timestamp.toString(Qt::DateFormat::ISODate);
-}
-
-QString Module::toString() const
-{
- QStringList sl;
-
- // file size
- sl.push_back(displayPath());
- sl.push_back(QString("%1 B").arg(m_fileSize));
-
- // version
- if (m_version.isEmpty() && m_versionString.isEmpty()) {
- sl.push_back("(no version)");
- } else {
- if (!m_version.isEmpty()) {
- sl.push_back(m_version);
- }
-
- if (!m_versionString.isEmpty() && m_versionString != m_version) {
- sl.push_back(versionString());
- }
- }
-
- // timestamp
- if (m_timestamp.isValid()) {
- sl.push_back(m_timestamp.toString(Qt::DateFormat::ISODate));
- } else {
- sl.push_back("(no timestamp)");
- }
-
- // md5
- if (!m_md5.isEmpty()) {
- sl.push_back(m_md5);
- }
-
- return sl.join(", ");
-}
-
-Module::FileInfo Module::getFileInfo() const
-{
- const auto wspath = m_path.toStdWString();
-
- // getting version info size
- DWORD dummy = 0;
- const DWORD size = GetFileVersionInfoSizeW(wspath.c_str(), &dummy);
-
- if (size == 0) {
- const auto e = GetLastError();
-
- if (e == ERROR_RESOURCE_TYPE_NOT_FOUND) {
- // not an error, no version information built into that module
- return {};
- }
-
- qCritical().nospace().noquote()
- << "GetFileVersionInfoSizeW() failed on '" << m_path << "', "
- << formatSystemMessageQ(e);
-
- return {};
- }
-
- // getting version info
- auto buffer = std::make_unique<std::byte[]>(size);
-
- if (!GetFileVersionInfoW(wspath.c_str(), 0, size, buffer.get())) {
- const auto e = GetLastError();
-
- qCritical().nospace().noquote()
- << "GetFileVersionInfoW() failed on '" << m_path << "', "
- << formatSystemMessageQ(e);
-
- return {};
- }
-
- // the version info has two major parts: a fixed version and a localizable
- // set of strings
-
- FileInfo fi;
- fi.ffi = getFixedFileInfo(buffer.get());
- fi.fileDescription = getFileDescription(buffer.get());
-
- return fi;
-}
-
-VS_FIXEDFILEINFO Module::getFixedFileInfo(std::byte* buffer) const
-{
- void* valuePointer = nullptr;
- unsigned int valueSize = 0;
-
- // the fixed version info is in the root
- const auto ret = VerQueryValueW(buffer, L"\\", &valuePointer, &valueSize);
-
- if (!ret || !valuePointer || valueSize == 0) {
- // not an error, no fixed file info
- return {};
- }
-
- const auto* fi = static_cast<VS_FIXEDFILEINFO*>(valuePointer);
-
- // signature is always 0xfeef04bd
- if (fi->dwSignature != 0xfeef04bd) {
- qCritical().nospace().noquote()
- << "bad file info signature 0x" << hex << fi->dwSignature << " for "
- << "'" << m_path << "'";
-
- return {};
- }
-
- return *fi;
-}
-
-QString Module::getFileDescription(std::byte* buffer) const
-{
- struct LANGANDCODEPAGE
- {
- WORD wLanguage;
- WORD wCodePage;
- };
-
- void* valuePointer = nullptr;
- unsigned int valueSize = 0;
-
- // getting list of available languages
- auto ret = VerQueryValueW(
- buffer, L"\\VarFileInfo\\Translation", &valuePointer, &valueSize);
-
- if (!ret || !valuePointer || valueSize == 0) {
- qCritical().nospace().noquote()
- << "VerQueryValueW() for translations failed on '" << m_path << "'";
-
- return {};
- }
-
- // number of languages
- const auto count = valueSize / sizeof(LANGANDCODEPAGE);
- if (count == 0) {
- return {};
- }
-
- // using the first language in the list to get FileVersion
- const auto* lcp = static_cast<LANGANDCODEPAGE*>(valuePointer);
-
- const auto subBlock = QString("\\StringFileInfo\\%1%2\\FileVersion")
- .arg(lcp->wLanguage, 4, 16, QChar('0'))
- .arg(lcp->wCodePage, 4, 16, QChar('0'));
-
- ret = VerQueryValueW(
- buffer, subBlock.toStdWString().c_str(), &valuePointer, &valueSize);
-
- if (!ret || !valuePointer || valueSize == 0) {
- // not an error, no file version
- return {};
- }
-
- // valueSize includes the null terminator
- return QString::fromWCharArray(
- static_cast<wchar_t*>(valuePointer), valueSize - 1);
-}
-
-QString Module::getVersion(const VS_FIXEDFILEINFO& fi) const
-{
- if (fi.dwSignature == 0) {
- return {};
- }
-
- const DWORD major = (fi.dwFileVersionMS >> 16 ) & 0xffff;
- const DWORD minor = (fi.dwFileVersionMS >> 0 ) & 0xffff;
- const DWORD maintenance = (fi.dwFileVersionLS >> 16 ) & 0xffff;
- const DWORD build = (fi.dwFileVersionLS >> 0 ) & 0xffff;
-
- if (major == 0 && minor == 0 && maintenance == 0 && build == 0) {
- return {};
- }
-
- return QString("%1.%2.%3.%4")
- .arg(major).arg(minor).arg(maintenance).arg(build);
-}
-
-QDateTime Module::getTimestamp(const VS_FIXEDFILEINFO& fi) const
-{
- FILETIME ft = {};
-
- if (fi.dwSignature == 0 || (fi.dwFileDateMS == 0 && fi.dwFileDateLS == 0)) {
- // if the file info is invalid or doesn't have a date, use the creation
- // time on the file
-
- // opening the file
- HandlePtr h(CreateFileW(
- m_path.toStdWString().c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr,
- OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0));
-
- if (h.get() == INVALID_HANDLE_VALUE) {
- const auto e = GetLastError();
-
- qCritical().nospace().noquote()
- << "can't open file '" << m_path << "' for timestamp, "
- << formatSystemMessageQ(e);
-
- return {};
- }
-
- // getting the file time
- if (!GetFileTime(h.get(), &ft, nullptr, nullptr)) {
- const auto e = GetLastError();
- qCritical().nospace().noquote()
- << "can't get file time for '" << m_path << "', "
- << formatSystemMessageQ(e);
-
- return {};
- }
- } else {
- // use the time from the file info
- ft.dwHighDateTime = fi.dwFileDateMS;
- ft.dwLowDateTime = fi.dwFileDateLS;
- }
-
-
- // converting to SYSTEMTIME
- SYSTEMTIME utc = {};
-
- if (!FileTimeToSystemTime(&ft, &utc)) {
- qCritical().nospace().noquote()
- << "FileTimeToSystemTime() failed on timestamp "
- << "high=0x" << hex << ft.dwHighDateTime << " "
- << "low=0x" << hex << ft.dwLowDateTime << " for "
- << "'" << m_path << "'";
-
- return {};
- }
-
- return QDateTime(
- QDate(utc.wYear, utc.wMonth, utc.wDay),
- QTime(utc.wHour, utc.wMinute, utc.wSecond, utc.wMilliseconds));
-}
-
-QString Module::getMD5() const
-{
- if (m_path.contains("\\windows\\", Qt::CaseInsensitive)) {
- // don't calculate md5 for system files, it's not really relevant and
- // it takes a while
- return {};
- }
-
- // opening the file
- QFile f(m_path);
-
- if (!f.open(QFile::ReadOnly)) {
- qCritical().nospace().noquote()
- << "failed to open file '" << m_path << "' for md5";
-
- return {};
- }
-
- // hashing
- QCryptographicHash hash(QCryptographicHash::Md5);
- if (!hash.addData(&f)) {
- qCritical().nospace().noquote()
- << "failed to calculate md5 for '" << m_path << "'";
-
- return {};
- }
-
- return hash.result().toHex();
-}
-
-
-WindowsInfo::WindowsInfo()
-{
- // loading ntdll.dll, the functions will be found with GetProcAddress()
- std::unique_ptr<HINSTANCE, LibraryFreer> ntdll(LoadLibraryW(L"ntdll.dll"));
-
- if (!ntdll) {
- qCritical() << "failed to load ntdll.dll while getting version";
- return;
- } else {
- m_reported = getReportedVersion(ntdll.get());
- m_real = getRealVersion(ntdll.get());
- }
-
- m_release = getRelease();
- m_elevated = getElevated();
-}
-
-bool WindowsInfo::compatibilityMode() const
-{
- if (m_real == Version()) {
- // don't know the real version, can't guess compatibility mode
- return false;
- }
-
- return (m_real != m_reported);
-}
-
-const WindowsInfo::Version& WindowsInfo::reportedVersion() const
-{
- return m_reported;
-}
-
-const WindowsInfo::Version& WindowsInfo::realVersion() const
-{
- return m_real;
-}
-
-const WindowsInfo::Release& WindowsInfo::release() const
-{
- return m_release;
-}
-
-std::optional<bool> WindowsInfo::isElevated() const
-{
- return m_elevated;
-}
-
-QString WindowsInfo::toString() const
-{
- QStringList sl;
-
- const QString reported = m_reported.toString();
- const QString real = m_real.toString();
-
- // version
- sl.push_back("version: " + reported);
-
- // real version if different
- if (compatibilityMode()) {
- sl.push_back("real version: " + real);
- }
-
- // build.UBR, such as 17763.557
- if (m_release.UBR != 0) {
- DWORD build = 0;
-
- if (compatibilityMode()) {
- build = m_real.build;
- } else {
- build = m_reported.build;
- }
-
- sl.push_back(QString("%1.%2").arg(build).arg(m_release.UBR));
- }
-
- // release ID
- if (!m_release.ID.isEmpty()) {
- sl.push_back("release " + m_release.ID);
- }
-
- // buildlab string
- if (!m_release.buildLab.isEmpty()) {
- sl.push_back(m_release.buildLab);
- }
-
- // product name
- if (!m_release.productName.isEmpty()) {
- sl.push_back(m_release.productName);
- }
-
- // elevated
- QString elevated = "?";
- if (m_elevated.has_value()) {
- elevated = (*m_elevated ? "yes" : "no");
- }
-
- sl.push_back("elevated: " + elevated);
-
- return sl.join(", ");
-}
-
-WindowsInfo::Version WindowsInfo::getReportedVersion(HINSTANCE ntdll) const
-{
- // windows has been deprecating pretty much all the functions having to do
- // with getting version information because apparently, people keep misusing
- // them for feature detection
- //
- // there's still RtlGetVersion() though
-
- using RtlGetVersionType = NTSTATUS (NTAPI)(PRTL_OSVERSIONINFOW);
-
- auto* RtlGetVersion = reinterpret_cast<RtlGetVersionType*>(
- GetProcAddress(ntdll, "RtlGetVersion"));
-
- if (!RtlGetVersion) {
- qCritical() << "RtlGetVersion() not found in ntdll.dll";
- return {};
- }
-
- OSVERSIONINFOEX vi = {};
- vi.dwOSVersionInfoSize = sizeof(vi);
-
- // this apparently never fails
- RtlGetVersion((RTL_OSVERSIONINFOW*)&vi);
-
- return {vi.dwMajorVersion, vi.dwMinorVersion, vi.dwBuildNumber};
-}
-
-WindowsInfo::Version WindowsInfo::getRealVersion(HINSTANCE ntdll) const
-{
- // getting the actual windows version is more difficult because all the
- // functions are lying when running in compatibility mode
- //
- // RtlGetNtVersionNumbers() is an undocumented function that seems to work
- // fine, but it might not in the future
-
- using RtlGetNtVersionNumbersType = void (NTAPI)(DWORD*, DWORD*, DWORD*);
-
- auto* RtlGetNtVersionNumbers = reinterpret_cast<RtlGetNtVersionNumbersType*>(
- GetProcAddress(ntdll, "RtlGetNtVersionNumbers"));
-
- if (!RtlGetNtVersionNumbers) {
- qCritical() << "RtlGetNtVersionNumbers not found in ntdll.dll";
- return {};
- }
-
- DWORD major=0, minor=0, build=0;
- RtlGetNtVersionNumbers(&major, &minor, &build);
-
- // for whatever reason, the build number has 0xf0000000 set
- build = 0x0fffffff & build;
-
- return {major, minor, build};
-}
-
-WindowsInfo::Release WindowsInfo::getRelease() const
-{
- // there are several interesting items in the registry, but most of them
- // are undocumented, not always available, and localizable
- //
- // most of them are used to provide as much information as possible in case
- // any of the other versions fail to work
-
- QSettings settings(
- R"(HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion)",
- QSettings::NativeFormat);
-
- Release r;
-
- // buildlab seems to be an internal name from the build system
- r.buildLab = settings.value("BuildLabEx", "").toString();
- if (r.buildLab.isEmpty()) {
- r.buildLab = settings.value("BuildLab", "").toString();
- if (r.buildLab.isEmpty()) {
- r.buildLab = settings.value("BuildBranch", "").toString();
- }
- }
-
- // localized name of windows, such as "Windows 10 Pro"
- r.productName = settings.value("ProductName", "").toString();
-
- // release ID, such as 1803
- r.ID = settings.value("ReleaseId", "").toString();
-
- // some other build number, shown in winver.exe
- r.UBR = settings.value("UBR", 0).toUInt();
-
- return r;
-}
-
-std::optional<bool> WindowsInfo::getElevated() const
-{
- HandlePtr token;
-
- {
- HANDLE rawToken = 0;
-
- if (!OpenProcessToken(GetCurrentProcess( ), TOKEN_QUERY, &rawToken)) {
- const auto e = GetLastError();
-
- qCritical()
- << "while trying to check if process is elevated, "
- << "OpenProcessToken() failed: " << formatSystemMessageQ(e);
-
- return {};
- }
-
- token.reset(rawToken);
- }
-
- TOKEN_ELEVATION e = {};
- DWORD size = sizeof(TOKEN_ELEVATION);
-
- if (!GetTokenInformation(token.get(), TokenElevation, &e, sizeof(e), &size)) {
- const auto e = GetLastError();
-
- qCritical()
- << "while trying to check if process is elevated, "
- << "GetTokenInformation() failed: " << formatSystemMessageQ(e);
-
- return {};
- }
-
- return (e.TokenIsElevated != 0);
-}
-
-
-SecurityProduct::SecurityProduct(
- QString name, int provider,
- bool active, bool upToDate) :
- m_name(std::move(name)), m_provider(provider),
- m_active(active), m_upToDate(upToDate)
-{
-}
-
-const QString& SecurityProduct::name() const
-{
- return m_name;
-}
-
-int SecurityProduct::provider() const
+QString getUsvfsDLLVersion()
{
- return m_provider;
-}
-
-bool SecurityProduct::active() const
-{
- return m_active;
-}
+ // once 2.2.2 is released, this can be changed to call USVFSVersionString()
+ // directly; until then, using GetProcAddress() allows for mixing up devbuilds
+ // and usvfs dlls
-bool SecurityProduct::upToDate() const
-{
- return m_upToDate;
-}
+ using USVFSVersionStringType = const char* WINAPI ();
-QString SecurityProduct::toString() const
-{
QString s;
- s += m_name + " ";
-
-
- QStringList ps;
- if (m_provider & WSC_SECURITY_PROVIDER_FIREWALL) {
- ps.push_back("firewall");
- }
-
- if (m_provider & WSC_SECURITY_PROVIDER_AUTOUPDATE_SETTINGS) {
- ps.push_back("autoupdate");
- }
-
- if (m_provider & WSC_SECURITY_PROVIDER_ANTIVIRUS) {
- ps.push_back("antivirus");
- }
-
- if (m_provider & WSC_SECURITY_PROVIDER_ANTISPYWARE) {
- ps.push_back("antispyware");
- }
+ const auto m = ::LoadLibraryW(L"usvfs_x64.dll");
- if (m_provider & WSC_SECURITY_PROVIDER_INTERNET_SETTINGS) {
- ps.push_back("settings");
- }
+ if (m) {
+ auto* f = reinterpret_cast<USVFSVersionStringType*>(
+ ::GetProcAddress(m, "USVFSVersionString"));
- if (m_provider & WSC_SECURITY_PROVIDER_USER_ACCOUNT_CONTROL) {
- ps.push_back("uac");
- }
-
- if (m_provider & WSC_SECURITY_PROVIDER_SERVICE) {
- ps.push_back("service");
- }
-
- if (ps.empty()) {
- s += "(doesn't provide anything)";
- } else {
- s += "(" + ps.join("|") + ")";
- }
+ if (f) {
+ s = f();
+ }
- if (m_active) {
- s += ", active";
- } else {
- s += ", inactive";
+ ::FreeLibrary(m);
}
- if (!m_upToDate) {
- s += ", definitions outdated";
+ if (s.isEmpty()) {
+ s = "?";
}
return s;
}
-
-struct Process
+QString getUsvfsVersionString()
{
- std::wstring filename;
- DWORD pid;
-
- Process(std::wstring f, DWORD id)
- : filename(std::move(f)), pid(id)
- {
- }
-};
+ const QString dll = getUsvfsDLLVersion();
+ const QString header = USVFS_VERSION_STRING;
-// returns the filename of the given process or the current one
-//
-std::wstring processFilename(HANDLE process=INVALID_HANDLE_VALUE)
-{
- // double the buffer size 10 times
- const int MaxTries = 10;
-
- DWORD bufferSize = MAX_PATH;
-
- for (int tries=0; tries<MaxTries; ++tries)
- {
- auto buffer = std::make_unique<wchar_t[]>(bufferSize + 1);
- std::fill(buffer.get(), buffer.get() + bufferSize + 1, 0);
-
- DWORD writtenSize = 0;
-
- if (process == INVALID_HANDLE_VALUE) {
- // query this process
- writtenSize = GetModuleFileNameW(0, buffer.get(), bufferSize);
- } else {
- // query another process
- writtenSize = GetModuleBaseNameW(process, 0, buffer.get(), bufferSize);
- }
-
- if (writtenSize == 0) {
- // hard failure
- const auto e = GetLastError();
- std::wcerr << formatSystemMessage(e) << L"\n";
- break;
- } else if (writtenSize >= bufferSize) {
- // buffer is too small, try again
- bufferSize *= 2;
- } else {
- // if GetModuleFileName() works, `writtenSize` does not include the null
- // terminator
- const std::wstring s(buffer.get(), writtenSize);
- const fs::path path(s);
-
- return path.filename().native();
- }
- }
+ QString usvfsVersion;
- // something failed or the path is way too long to make sense
-
- std::wstring what;
- if (process == INVALID_HANDLE_VALUE) {
- what = L"the current process";
+ if (dll == header) {
+ return dll;
} else {
- what = L"pid " + std::to_wstring(reinterpret_cast<std::uintptr_t>(process));
+ return "dll is " + dll + ", compiled against " + header;
}
-
- std::wcerr << L"failed to get filename for " << what << L"\n";
- return {};
}
-std::vector<DWORD> runningProcessesIds()
-{
- // double the buffer size 10 times
- const int MaxTries = 10;
-
- // initial size of 300 processes, unlikely to be more than that
- std::size_t size = 300;
-
- for (int tries=0; tries<MaxTries; ++tries) {
- auto ids = std::make_unique<DWORD[]>(size);
- std::fill(ids.get(), ids.get() + size, 0);
-
- DWORD bytesGiven = static_cast<DWORD>(size * sizeof(ids[0]));
- DWORD bytesWritten = 0;
-
- if (!EnumProcesses(ids.get(), bytesGiven, &bytesWritten))
- {
- const auto e = GetLastError();
-
- std::wcerr
- << L"failed to enumerate processes, "
- << formatSystemMessage(e) << L"\n";
-
- return {};
- }
-
- if (bytesWritten == bytesGiven) {
- // no way to distinguish between an exact fit and not enough space,
- // just try again
- size *= 2;
- continue;
- }
+} // namespace MOShared
- const auto count = bytesWritten / sizeof(ids[0]);
- return std::vector<DWORD>(ids.get(), ids.get() + count);
- }
- std::cerr << L"too many processes to enumerate";
- return {};
-}
+static bool g_exiting = false;
+static bool g_canClose = false;
-std::vector<Process> runningProcesses()
+MainWindow* findMainWindow()
{
- const auto pids = runningProcessesIds();
- std::vector<Process> v;
-
- for (const auto& pid : pids) {
- if (pid == 0) {
- // the idle process has pid 0 and seems to be picked up by EnumProcesses()
- continue;
- }
-
- HandlePtr h(OpenProcess(
- PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, pid));
-
- if (!h) {
- const auto e = GetLastError();
-
- if (e != ERROR_ACCESS_DENIED) {
- // don't log access denied, will happen a lot for system processes, even
- // when elevated
- std::wcerr
- << L"failed to open process " << pid << L", "
- << formatSystemMessage(e) << L"\n";
- }
-
- continue;
- }
-
- auto filename = processFilename(h.get());
- if (!filename.empty()) {
- v.emplace_back(std::move(filename), pid);
+ for (auto* tl : qApp->topLevelWidgets()) {
+ if (auto* mw=dynamic_cast<MainWindow*>(tl)) {
+ return mw;
}
}
- return v;
+ return nullptr;
}
-DWORD findOtherPid()
+bool ExitModOrganizer(ExitFlags e)
{
- const std::wstring defaultName = L"ModOrganizer.exe";
-
- std::wclog << L"looking for the other process...\n";
-
- // used to skip the current process below
- const auto thisPid = GetCurrentProcessId();
- std::wclog << L"this process id is " << thisPid << L"\n";
-
- // getting the filename for this process, assumes the other process has the
- // smae one
- auto filename = processFilename();
- if (filename.empty()) {
- std::wcerr
- << L"can't get current process filename, defaulting to "
- << defaultName << L"\n";
-
- filename = defaultName;
- } else {
- std::wclog << L"this process filename is " << filename << L"\n";
+ if (g_exiting) {
+ return true;
}
- // getting all running processes
- const auto processes = runningProcesses();
- std::wclog << L"there are " << processes.size() << L" processes running\n";
+ g_exiting = true;
+ MOBase::Guard g([&]{ g_exiting = false; });
- // going through processes, trying to find one with the same name and a
- // different pid than this process has
- for (const auto& p : processes) {
- if (p.filename == filename) {
- if (p.pid != thisPid) {
- return p.pid;
+ if (!e.testFlag(Exit::Force)) {
+ if (auto* mw=findMainWindow()) {
+ if (!mw->canExit()) {
+ return false;
}
}
}
- std::wclog
- << L"no process with this filename\n"
- << L"MO may not be running, or it may be running as administrator\n"
- << L"you can try running this again as administrator\n";
-
- return 0;
-}
-
-std::wstring tempDir()
-{
- const DWORD bufferSize = MAX_PATH + 1;
- wchar_t buffer[bufferSize + 1] = {};
-
- const auto written = GetTempPathW(bufferSize, buffer);
- if (written == 0) {
- const auto e = GetLastError();
-
- std::wcerr
- << L"failed to get temp path, " << formatSystemMessage(e) << L"\n";
-
- return {};
- }
-
- // `written` does not include the null terminator
- return std::wstring(buffer, buffer + written);
-}
-
-HandlePtr tempFile(const std::wstring dir)
-{
- // maximum tries of incrementing the counter
- const int MaxTries = 100;
-
- // UTC time and date will be in the filename
- const auto now = std::time(0);
- const auto tm = std::gmtime(&now);
-
- // "ModOrganizer-YYYYMMDDThhmmss.dmp", with a possible "-i" appended, where
- // i can go until MaxTries
- std::wostringstream oss;
- oss
- << L"ModOrganizer-"
- << std::setw(4) << (1900 + tm->tm_year)
- << std::setw(2) << std::setfill(L'0') << (tm->tm_mon + 1)
- << std::setw(2) << std::setfill(L'0') << tm->tm_mday << "T"
- << std::setw(2) << std::setfill(L'0') << tm->tm_hour
- << std::setw(2) << std::setfill(L'0') << tm->tm_min
- << std::setw(2) << std::setfill(L'0') << tm->tm_sec;
-
- const std::wstring prefix = oss.str();
- const std::wstring ext = L".dmp";
-
- // first path to try, without counter in it
- std::wstring path = dir + L"\\" + prefix + ext;
+ g_canClose = true;
- for (int i=0; i<MaxTries; ++i) {
- std::wclog << L"trying file '" << path << L"'\n";
+ const int code = (e.testFlag(Exit::Restart) ? RestartExitCode : 0);
+ qApp->exit(code);
- HandlePtr h (CreateFileW(
- path.c_str(), GENERIC_WRITE, 0, nullptr,
- CREATE_NEW, FILE_ATTRIBUTE_NORMAL, nullptr));
-
- if (h.get() != INVALID_HANDLE_VALUE) {
- // worked
- return h;
- }
-
- const auto e = GetLastError();
-
- if (e != ERROR_FILE_EXISTS) {
- // probably no write access
- std::wcerr
- << L"failed to create dump file, " << formatSystemMessage(e) << L"\n";
-
- return {};
- }
-
- // try again with "-i"
- path = dir + L"\\" + prefix + L"-" + std::to_wstring(i + 1) + ext;
- }
-
- std::wcerr << L"can't create dump file, ran out of filenames\n";
- return {};
-}
-
-HandlePtr dumpFile()
-{
- // try the current directory
- HandlePtr h = tempFile(L".");
- if (h.get() != INVALID_HANDLE_VALUE) {
- return h;
- }
-
- std::wclog << L"cannot write dump file in current directory\n";
-
- // try the temp directory
- const auto dir = tempDir();
-
- if (!dir.empty()) {
- h = tempFile(dir.c_str());
- if (h.get() != INVALID_HANDLE_VALUE) {
- return h;
- }
- }
-
- return {};
+ return true;
}
-bool createMiniDump(HANDLE process, CoreDumpTypes type)
+bool ModOrganizerCanCloseNow()
{
- const DWORD pid = GetProcessId(process);
-
- const HandlePtr file = dumpFile();
- if (!file) {
- std::wcerr << L"nowhere to write the dump file\n";
- return false;
- }
-
- auto flags = _MINIDUMP_TYPE(
- MiniDumpNormal |
- MiniDumpWithHandleData |
- MiniDumpWithUnloadedModules |
- MiniDumpWithProcessThreadData);
-
- if (type == CoreDumpTypes::Data) {
- std::wclog << L"writing minidump with data\n";
- flags = _MINIDUMP_TYPE(flags | MiniDumpWithDataSegs);
- } else if (type == CoreDumpTypes::Full) {
- std::wclog << L"writing full minidump\n";
- flags = _MINIDUMP_TYPE(flags | MiniDumpWithFullMemory);
- } else {
- std::wclog << L"writing mini minidump\n";
- }
-
- const auto ret = MiniDumpWriteDump(
- process, pid, file.get(), flags, nullptr, nullptr, nullptr);
-
- if (!ret) {
- const auto e = GetLastError();
-
- std::wcerr
- << L"failed to write mini dump, " << formatSystemMessage(e) << L"\n";
-
- return false;
- }
-
- std::wclog << L"minidump written correctly\n";
- return true;
+ return g_canClose;
}
-
-bool coredump(CoreDumpTypes type)
+bool ModOrganizerExiting()
{
- std::wclog << L"creating minidump for the current process\n";
- return createMiniDump(GetCurrentProcess(), type);
+ return g_exiting;
}
-bool coredumpOther(CoreDumpTypes type)
+void ResetExitFlag()
{
- std::wclog << L"creating minidump for an running process\n";
-
- const auto pid = findOtherPid();
- if (pid == 0) {
- std::wcerr << L"no other process found\n";
- return false;
- }
-
- std::wclog << L"found other process with pid " << pid << L"\n";
-
- HandlePtr handle(OpenProcess(
- PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, pid));
-
- if (!handle) {
- const auto e = GetLastError();
-
- std::wcerr
- << L"failed to open process " << pid << L", "
- << formatSystemMessage(e) << L"\n";
-
- return false;
- }
-
- return createMiniDump(handle.get(), type);
+ g_exiting = false;
}
-
-} // namespace env
-
-} // namespace MOShared
diff --git a/src/shared/util.h b/src/shared/util.h
index c4a2ed7d..e8a58549 100644
--- a/src/shared/util.h
+++ b/src/shared/util.h
@@ -20,13 +20,7 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#ifndef UTIL_H
#define UTIL_H
-
#include <string>
-#include <optional>
-
-#define WIN32_LEAN_AND_MEAN
-#include <Windows.h>
-
#include <versioninfo.h>
class Executable;
@@ -50,396 +44,28 @@ std::wstring ToLower(const std::wstring &text);
bool CaseInsensitiveEqual(const std::wstring &lhs, const std::wstring &rhs);
+MOBase::VersionInfo createVersionInfo();
+QString getUsvfsVersionString();
-namespace env
-{
-
-// an application shortcut that can be either on the desktop or the start menu
-//
-class Shortcut
-{
-public:
- // location of a shortcut
- //
- enum Locations
- {
- None = 0,
-
- // on the desktop
- Desktop,
-
- // in the start menu
- StartMenu
- };
-
-
- // empty shortcut
- //
- Shortcut();
-
- // shortcut from an executable
- //
- explicit Shortcut(const Executable& exe);
-
- // sets the name of the shortcut, shown on icons and start menu entries
- //
- Shortcut& name(const QString& s);
-
- // the program to start
- //
- Shortcut& target(const QString& s);
-
- // arguments to pass
- //
- Shortcut& arguments(const QString& s);
-
- // shows in the status bar of explorer, for example
- //
- Shortcut& description(const QString& s);
-
- // path to a binary that contains the icon and its index
- //
- Shortcut& icon(const QString& s, int index=0);
-
- // "start in" option for this shortcut
- //
- Shortcut& workingDirectory(const QString& s);
-
-
- // returns whether this shortcut already exists at the given location; this
- // does not check whether the shortcut parameters are different, it merely if
- // the .lnk file exists
- //
- bool exists(Locations loc) const;
-
- // calls remove() if exists(), or add()
- //
- bool toggle(Locations loc);
-
- // adds the shortcut to the given location
- //
- bool add(Locations loc);
-
- // removes the shortcut from the given location
- //
- bool remove(Locations loc);
-
-private:
- QString m_name;
- QString m_target;
- QString m_arguments;
- QString m_description;
- QString m_icon;
- int m_iconIndex;
- QString m_workingDirectory;
-
- // returns a qCritical() logger with a prefix already logged
- //
- QDebug critical() const;
-
- // returns a qDebug() logger with a prefix already logged
- //
- QDebug debug() const;
-
-
- // returns the path where the shortcut file should be saved
- //
- QString shortcutPath(Locations loc) const;
-
- // returns the directory where the shortcut file should be saved
- //
- QString shortcutDirectory(Locations loc) const;
-
- // returns the filename of the shortcut file that should be used when saving
- //
- QString shortcutFilename() const;
-};
-
-
-// returns a string representation of the given location
-//
-QString toString(Shortcut::Locations loc);
-
-
-// represents one module
-//
-class Module
-{
-public:
- explicit Module(QString path, std::size_t fileSize);
-
- // returns the module's path
- //
- const QString& path() const;
-
- // returns the module's path in lowercase and using forward slashes
- //
- QString displayPath() const;
-
- // returns the size in bytes, may be 0
- //
- std::size_t fileSize() const;
-
- // returns the x.x.x.x version embedded from the version info, may be empty
- //
- const QString& version() const;
-
- // returns the FileVersion entry from the resource file, returns
- // "(no version)" if not available
- //
- const QString& versionString() const;
-
- // returns the build date from the version info, or the creation time of the
- // file on the filesystem, may be empty
- //
- const QDateTime& timestamp() const;
-
- // returns the md5 of the file, may be empty for system files
- //
- const QString& md5() const;
-
- // converts timestamp() to a string for display, returns "(no timestamp)" if
- // not available
- //
- QString timestampString() const;
-
- // returns a string with all the above information on one line
- //
- QString toString() const;
-
-private:
- // contains the information from the version resource
- //
- struct FileInfo
- {
- VS_FIXEDFILEINFO ffi;
- QString fileDescription;
- };
-
- QString m_path;
- std::size_t m_fileSize;
- QString m_version;
- QDateTime m_timestamp;
- QString m_versionString;
- QString m_md5;
-
- // returns information from the version resource
- //
- FileInfo getFileInfo() const;
-
- // uses VS_FIXEDFILEINFO to build the version string
- //
- QString getVersion(const VS_FIXEDFILEINFO& fi) const;
-
- // uses the file date from VS_FIXEDFILEINFO if available, or gets the
- // creation date on the file
- //
- QDateTime getTimestamp(const VS_FIXEDFILEINFO& fi) const;
-
- // returns the md5 hash unless the path contains "\windows\"
- //
- QString getMD5() const;
-
- // gets VS_FIXEDFILEINFO from the file version info buffer
- //
- VS_FIXEDFILEINFO getFixedFileInfo(std::byte* buffer) const;
-
- // gets FileVersion from the file version info buffer
- //
- QString getFileDescription(std::byte* buffer) const;
-};
-
-
-// a variety of information on windows
-//
-class WindowsInfo
-{
-public:
- struct Version
- {
- DWORD major=0, minor=0, build=0;
-
- QString toString() const
- {
- return QString("%1.%2.%3").arg(major).arg(minor).arg(build);
- }
-
- friend bool operator==(const Version& a, const Version& b)
- {
- return
- a.major == b.major &&
- a.minor == b.minor &&
- a.build == b.build;
- }
-
- friend bool operator!=(const Version& a, const Version& b)
- {
- return !(a == b);
- }
- };
-
- struct Release
- {
- // the BuildLab entry from the registry, may be empty
- QString buildLab;
-
- // product name such as "Windows 10 Pro", may not be in English, may be
- // empty
- QString productName;
-
- // release ID such as 1809, may be mepty
- QString ID;
-
- // some sub-build number, undocumented, may be empty
- DWORD UBR;
-
- Release()
- : UBR(0)
- {
- }
- };
-
-
- WindowsInfo();
-
- // tries to guess whether this process is running in compatibility mode
- //
- bool compatibilityMode() const;
-
- // returns the Windows version, may not correspond to the actual version
- // if the process is running in compatibility mode
- //
- const Version& reportedVersion() const;
-
- // tries to guess the real Windows version that's running, can be empty
- //
- const Version& realVersion() const;
-
- // various information about the current release
- //
- const Release& release() const;
-
- // whether this process is running as administrator, may be empty if the
- // information is not available
- std::optional<bool> isElevated() const;
-
- // returns a string with all the above information on one line
- //
- QString toString() const;
-
-private:
- Version m_reported, m_real;
- Release m_release;
- std::optional<bool> m_elevated;
-
- // uses RtlGetVersion() to get the version number as reported by Windows
- //
- Version getReportedVersion(HINSTANCE ntdll) const;
-
- // uses RtlGetNtVersionNumbers() to get the real version number
- //
- Version getRealVersion(HINSTANCE ntdll) const;
-
- // gets various information from the registry
- //
- Release getRelease() const;
-
- // gets whether the process is elevated
- //
- std::optional<bool> getElevated() const;
-};
-
-
-// represents a security product, such as an antivirus or a firewall
-//
-class SecurityProduct
-{
-public:
- SecurityProduct(
- QString name, int provider,
- bool active, bool upToDate);
-
- // display name of the product
- //
- const QString& name() const;
-
- // a bunch of _WSC_SECURITY_PROVIDER flags
- //
- int provider() const;
-
- // whether the product is active
- //
- bool active() const;
-
- // whether its definitions are up-to-date
- //
- bool upToDate() const;
-
- // string representation of the above
- //
- QString toString() const;
-
-private:
- QString m_name;
- int m_provider;
- bool m_active;
- bool m_upToDate;
-};
-
-
-// represents the process's environment
-//
-class Environment
-{
-public:
- Environment();
-
- // list of loaded modules in the current process
- //
- const std::vector<Module>& loadedModules();
-
- // information about the operating system
- //
- const WindowsInfo& windowsInfo() const;
-
- // information about the installed security products
- //
- const std::vector<SecurityProduct>& securityProducts() const;
-
-private:
- std::vector<Module> m_modules;
- WindowsInfo m_windows;
- std::vector<SecurityProduct> m_security;
-
- std::vector<Module> getLoadedModules() const;
- std::vector<SecurityProduct> getSecurityProducts() const;
-
- std::vector<SecurityProduct> getSecurityProductsFromWMI() const;
- std::optional<SecurityProduct> getWindowsFirewall() const;
-};
+} // namespace MOShared
-enum class CoreDumpTypes
+enum class Exit
{
- Mini = 1,
- Data,
- Full
+ None = 0x00,
+ Normal = 0x01,
+ Restart = 0x02,
+ Force = 0x04
};
-// creates a minidump file for the given process
-//
-bool coredump(CoreDumpTypes type);
-
-// finds another process with the same name as this one and creates a minidump
-// file for it
-//
-bool coredumpOther(CoreDumpTypes type);
-
-} // namespace env
+const int RestartExitCode = INT_MAX;
+using ExitFlags = QFlags<Exit>;
+Q_DECLARE_OPERATORS_FOR_FLAGS(ExitFlags);
-MOBase::VersionInfo createVersionInfo();
-
-} // namespace MOShared
+bool ExitModOrganizer(ExitFlags e=Exit::Normal);
+bool ModOrganizerExiting();
+bool ModOrganizerCanCloseNow();
+void ResetExitFlag();
#endif // UTIL_H
diff --git a/src/singleinstance.cpp b/src/singleinstance.cpp
index 7abf3879..aa62d40a 100644
--- a/src/singleinstance.cpp
+++ b/src/singleinstance.cpp
@@ -20,6 +20,7 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#include "singleinstance.h"
#include "utility.h"
#include <report.h>
+#include <log.h>
#include <QLocalSocket>
static const char s_Key[] = "mo-43d1a3ad-eeb0-4818-97c9-eda5216c29b5";
@@ -93,17 +94,33 @@ void SingleInstance::sendMessage(const QString &message)
}
socket.disconnectFromServer();
+ socket.waitForDisconnected();
}
void SingleInstance::receiveMessage()
{
QLocalSocket *socket = m_Server.nextPendingConnection();
- if (!socket->waitForReadyRead(s_Timeout)) {
- reportError(tr("failed to receive data from secondary instance: %1").arg(socket->errorString()));
+ if (!socket) {
return;
}
+ if (!socket->waitForReadyRead(s_Timeout)) {
+ // check if there are bytes available; if so, it probably means the data was
+ // already received by the time waitForReadyRead() was called and the
+ // connection has been closed
+ const auto av = socket->bytesAvailable();
+
+ if (av <= 0) {
+ MOBase::log::error(
+ "failed to receive data from secondary instance: {}",
+ socket->errorString());
+
+ reportError(tr("failed to receive data from secondary instance: %1").arg(socket->errorString()));
+ return;
+ }
+ }
+
QString message = QString::fromUtf8(socket->readAll().constData());
emit messageSent(message);
socket->disconnectFromServer();
diff --git a/src/spawn.cpp b/src/spawn.cpp
index f77da35f..33bdbc05 100644
--- a/src/spawn.cpp
+++ b/src/spawn.cpp
@@ -21,164 +21,1043 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#include "report.h"
#include "utility.h"
+#include "env.h"
+#include "envwindows.h"
+#include "envsecurity.h"
+#include "envmodule.h"
+#include "settings.h"
+#include "settingsdialogworkarounds.h"
+#include <errorcodes.h>
#include <report.h>
+#include <log.h>
#include <usvfs.h>
#include <Shellapi.h>
#include <appconfig.h>
#include <windows_error.h>
-#include "helper.h"
-
#include <QApplication>
#include <QMessageBox>
#include <QtDebug>
-
-
#include <Shellapi.h>
-
-#include <boost/scoped_array.hpp>
+#include <fmt/format.h>
using namespace MOBase;
using namespace MOShared;
+namespace spawn::dialogs
+{
+
+std::wstring makeRightsDetails(const env::FileSecurity& fs)
+{
+ if (fs.rights.normalRights) {
+ return L"(normal rights)";
+ }
+
+ if (fs.rights.list.isEmpty()) {
+ return L"(none)";
+ }
+
+ std::wstring s = fs.rights.list.join("|").toStdWString();
+ if (!fs.rights.hasExecute) {
+ s += L" (execute is missing)";
+ }
+
+ return s;
+}
+
+QString makeDetails(const SpawnParameters& sp, DWORD code, const QString& more={})
+{
+ std::wstring owner, rights;
+
+ if (sp.binary.isFile()) {
+ const auto fs = env::getFileSecurity(sp.binary.absoluteFilePath());
+
+ if (fs.error.isEmpty()) {
+ owner = fs.owner.toStdWString();
+ rights = makeRightsDetails(fs);
+ } else {
+ owner = fs.error.toStdWString();
+ rights = fs.error.toStdWString();
+ }
+ } else {
+ owner = L"(file not found)";
+ rights = L"(file not found)";
+ }
+
+ const bool cwdExists = (sp.currentDirectory.isEmpty() ?
+ true : sp.currentDirectory.exists());
+
+ const auto appDir = QCoreApplication::applicationDirPath();
+ const auto sep = QDir::separator();
+
+ const std::wstring usvfs_x86_dll =
+ QFileInfo(appDir + sep + "usvfs_x86.dll").isFile() ? L"ok" : L"not found";
+
+ const std::wstring usvfs_x64_dll =
+ QFileInfo(appDir + sep + "usvfs_x64.dll").isFile() ? L"ok" : L"not found";
+
+ const std::wstring usvfs_x86_proxy =
+ QFileInfo(appDir + sep + "usvfs_proxy_x86.exe").isFile() ? L"ok" : L"not found";
+
+ const std::wstring usvfs_x64_proxy =
+ QFileInfo(appDir + sep + "usvfs_proxy_x64.exe").isFile() ? L"ok" : L"not found";
+
+ std::wstring elevated;
+ if (auto b=env::Environment().windowsInfo().isElevated()) {
+ elevated = (*b ? L"yes" : L"no");
+ } else {
+ elevated = L"(not available)";
+ }
+
+ std::wstring f =
+ L"Error {code} {codename}{more}: {error}\n"
+ L" . binary: '{bin}'\n"
+ L" . owner: {owner}\n"
+ L" . rights: {rights}\n"
+ L" . arguments: '{args}'\n"
+ L" . cwd: '{cwd}'{cwdexists}\n"
+ L" . stdout: {stdout}, stderr: {stderr}, hooked: {hooked}\n"
+ L" . MO elevated: {elevated}";
+
+ if (sp.hooked) {
+ f += L"\n . usvfs x86:{x86_dll} x64:{x64_dll} proxy_x86:{x86_proxy} proxy_x64:{x64_proxy}";
+ }
+
+ const std::wstring wmore = (more.isEmpty() ? L"" : (", " + more).toStdWString());
+
+ const auto s = fmt::format(f,
+ fmt::arg(L"code", code),
+ fmt::arg(L"codename", errorCodeName(code)),
+ fmt::arg(L"more", wmore),
+ fmt::arg(L"bin", QDir::toNativeSeparators(sp.binary.absoluteFilePath()).toStdWString()),
+ fmt::arg(L"owner", owner),
+ fmt::arg(L"rights", rights),
+ fmt::arg(L"error", formatSystemMessage(code)),
+ fmt::arg(L"args", sp.arguments.toStdWString()),
+ fmt::arg(L"cwd", QDir::toNativeSeparators(sp.currentDirectory.absolutePath()).toStdWString()),
+ fmt::arg(L"cwdexists", (cwdExists ? L"" : L" (not found)")),
+ fmt::arg(L"stdout", (sp.stdOut == INVALID_HANDLE_VALUE ? L"no" : L"yes")),
+ fmt::arg(L"stderr", (sp.stdErr == INVALID_HANDLE_VALUE ? L"no" : L"yes")),
+ fmt::arg(L"hooked", (sp.hooked ? L"yes" : L"no")),
+ fmt::arg(L"x86_dll", usvfs_x86_dll),
+ fmt::arg(L"x64_dll", usvfs_x64_dll),
+ fmt::arg(L"x86_proxy", usvfs_x86_proxy),
+ fmt::arg(L"x64_proxy", usvfs_x64_proxy),
+ fmt::arg(L"elevated", elevated));
+
+ return QString::fromStdWString(s);
+}
+
+QString makeContent(const SpawnParameters& sp, DWORD code)
+{
+ if (code == ERROR_INVALID_PARAMETER) {
+ return QObject::tr(
+ "This error typically happens because an antivirus has deleted critical "
+ "files from Mod Organizer's installation folder or has made them "
+ "generally inaccessible. Add an exclusion for Mod Organizer's "
+ "installation folder in your antivirus, reinstall Mod Organizer and try "
+ "again.");
+ } else if (code == ERROR_ACCESS_DENIED) {
+ return QObject::tr(
+ "This error typically happens because an antivirus is preventing Mod "
+ "Organizer from starting programs. Add an exclusion for Mod Organizer's "
+ "installation folder in your antivirus and try again.");
+ } else if (code == ERROR_FILE_NOT_FOUND) {
+ return QObject::tr("The file '%1' does not exist.")
+ .arg(QDir::toNativeSeparators(sp.binary.absoluteFilePath()));
+ } else {
+ return QString::fromStdWString(formatSystemMessage(code));
+ }
+}
+
+QMessageBox::StandardButton badSteamReg(
+ QWidget* parent, const QString& keyName, const QString& valueName)
+{
+ const auto details = QString(
+ "can't start steam, registry value at '%1' is empty or doesn't exist")
+ .arg(keyName + "\\" + valueName);
+
+ log::error("{}", details);
+
+ return MOBase::TaskDialog(parent, QObject::tr("Cannot start Steam"))
+ .main(QObject::tr("Cannot start Steam"))
+ .content(QObject::tr(
+ "The path to the Steam executable cannot be found. You might try "
+ "reinstalling Steam."))
+ .details(details)
+ .icon(QMessageBox::Critical)
+ .button({
+ QObject::tr("Continue without starting Steam"),
+ QObject::tr("The program may fail to launch."),
+ QMessageBox::Yes})
+ .button({
+ QObject::tr("Cancel"),
+ QMessageBox::Cancel})
+ .exec();
+}
+
+QMessageBox::StandardButton startSteamFailed(
+ QWidget* parent,
+ const QString& keyName, const QString& valueName, const QString& exe,
+ const SpawnParameters& sp, DWORD e)
+{
+ auto details = QString(
+ "a steam install was found in the registry at '%1': '%2'\n\n")
+ .arg(keyName + "\\" + valueName)
+ .arg(exe);
+
+ details += makeDetails(sp, e);
+
+ log::error("{}", details);
+
+ return MOBase::TaskDialog(parent, QObject::tr("Cannot start Steam"))
+ .main(QObject::tr("Cannot start Steam"))
+ .content(makeContent(sp, e))
+ .details(details)
+ .icon(QMessageBox::Critical)
+ .button({
+ QObject::tr("Continue without starting Steam"),
+ QObject::tr("The program may fail to launch."),
+ QMessageBox::Yes})
+ .button({
+ QObject::tr("Cancel"),
+ QMessageBox::Cancel})
+ .exec();
+}
+
+void spawnFailed(QWidget* parent, const SpawnParameters& sp, DWORD code)
+{
+ const auto details = makeDetails(sp, code);
+ log::error("{}", details);
+
+ const auto title = QObject::tr("Cannot launch program");
+
+ const auto mainText = QObject::tr("Cannot start %1")
+ .arg(sp.binary.fileName());
+
+ MOBase::TaskDialog(parent, title)
+ .main(mainText)
+ .content(makeContent(sp, code))
+ .details(details)
+ .icon(QMessageBox::Critical)
+ .exec();
+}
+
+void helperFailed(
+ QWidget* parent, DWORD code, const QString& why, const std::wstring& binary,
+ const std::wstring& cwd, const std::wstring& args)
+{
+ SpawnParameters sp;
+ sp.binary = QString::fromStdWString(binary);
+ sp.currentDirectory.setPath(QString::fromStdWString(cwd));
+ sp.arguments = QString::fromStdWString(args);
+
+ const auto details = makeDetails(sp, code, "in " + why);
+ log::error("{}", details);
+
+ const auto title = QObject::tr("Cannot launch helper");
+
+ const auto mainText = QObject::tr("Cannot start %1")
+ .arg(sp.binary.fileName());
+
+ MOBase::TaskDialog(parent, title)
+ .main(mainText)
+ .content(makeContent(sp, code))
+ .details(details)
+ .icon(QMessageBox::Critical)
+ .exec();
+}
+
+bool confirmRestartAsAdmin(QWidget* parent, const SpawnParameters& sp)
+{
+ const auto details = makeDetails(sp, ERROR_ELEVATION_REQUIRED);
+
+ log::error("{}", details);
+
+ const auto title = QObject::tr("Elevation required");
+
+ const auto mainText = QObject::tr("Cannot start %1")
+ .arg(sp.binary.fileName());
+
+ const auto content = QObject::tr(
+ "This program is requesting to run as administrator but Mod Organizer "
+ "itself is not running as administrator. Running programs as administrator "
+ "is typically unnecessary as long as the game and Mod Organizer have been "
+ "installed outside \"Program Files\".\r\n\r\n"
+ "You can restart Mod Organizer as administrator and try launching the "
+ "program again.");
+
+ log::debug("asking user to restart MO as administrator");
+
+ const auto r = MOBase::TaskDialog(parent, title)
+ .main(mainText)
+ .content(content)
+ .details(details)
+ .icon(QMessageBox::Question)
+ .button({
+ QObject::tr("Restart Mod Organizer as administrator"),
+ QObject::tr("You must allow \"helper.exe\" to make changes to the system."),
+ QMessageBox::Yes})
+ .button({
+ QObject::tr("Cancel"),
+ QMessageBox::Cancel})
+ .exec();
+
+ return (r == QMessageBox::Yes);
+}
+
+QMessageBox::StandardButton confirmStartSteam(
+ QWidget* parent, const SpawnParameters& sp, const QString& details)
+{
+ const auto title = QObject::tr("Launch Steam");
+ const auto mainText = QObject::tr("This program requires Steam");
+ const auto content = QObject::tr(
+ "Mod Organizer has detected that this program likely requires Steam to be "
+ "running to function properly.");
+
+ return MOBase::TaskDialog(parent, title)
+ .main(mainText)
+ .content(content)
+ .details(details)
+ .icon(QMessageBox::Question)
+ .button({
+ QObject::tr("Start Steam"),
+ QMessageBox::Yes})
+ .button({
+ QObject::tr("Continue without starting Steam"),
+ QObject::tr("The program might fail to run."),
+ QMessageBox::No})
+ .button({
+ QObject::tr("Cancel"),
+ QMessageBox::Cancel})
+ .remember("steamQuery", sp.binary.fileName())
+ .exec();
+}
+
+QMessageBox::StandardButton confirmRestartAsAdminForSteam(
+ QWidget* parent, const SpawnParameters& sp)
+{
+ const auto title = QObject::tr("Elevation required");
+ const auto mainText = QObject::tr("Steam is running as administrator");
+ const auto content = QObject::tr(
+ "Running Steam as administrator is typically unnecessary and can cause "
+ "problems when Mod Organizer itself is not running as administrator."
+ "\r\n\r\n"
+ "You can restart Mod Organizer as administrator and try launching the "
+ "program again.");
-static const int BUFSIZE = 4096;
+ return MOBase::TaskDialog(parent, title)
+ .main(mainText)
+ .content(content)
+ .icon(QMessageBox::Question)
+ .button({
+ QObject::tr("Restart Mod Organizer as administrator"),
+ QObject::tr("You must allow \"helper.exe\" to make changes to the system."),
+ QMessageBox::Yes})
+ .button({
+ QObject::tr("Continue"),
+ QObject::tr("The program might fail to run."),
+ QMessageBox::No})
+ .button({
+ QObject::tr("Cancel"),
+ QMessageBox::Cancel})
+ .remember("steamAdminQuery", sp.binary.fileName())
+ .exec();
+}
-static bool spawn(LPCWSTR binary, LPCWSTR arguments, LPCWSTR currentDirectory,
- bool suspended, bool hooked,
- HANDLE stdOut, HANDLE stdErr,
- HANDLE& processHandle, HANDLE& threadHandle)
+bool eventLogNotRunning(
+ QWidget* parent, const env::Service& s, const SpawnParameters& sp)
+{
+ const auto title = QObject::tr("Event Log not running");
+ const auto mainText = QObject::tr("The Event Log service is not running");
+ const auto content = QObject::tr(
+ "The Windows Event Log service is not running. This can prevent USVFS from "
+ "running properly and your mods may not be recognized by the program being "
+ "launched.");
+
+ const auto r = MOBase::TaskDialog(parent, title)
+ .main(mainText)
+ .content(content)
+ .details(s.toString())
+ .icon(QMessageBox::Question)
+ .remember("eventLogService", sp.binary.fileName())
+ .button({
+ QObject::tr("Continue"),
+ QObject::tr("Your mods might not work."),
+ QMessageBox::Yes})
+ .button({
+ QObject::tr("Cancel"),
+ QMessageBox::Cancel})
+ .exec();
+
+ return (r == QMessageBox::Yes);
+}
+
+QMessageBox::StandardButton confirmBlacklisted(
+ QWidget* parent, const SpawnParameters& sp, Settings& settings)
+{
+ const auto title = QObject::tr("Blacklisted program");
+ const auto mainText = QObject::tr("The program %1 is blacklisted")
+ .arg(sp.binary.fileName());
+ const auto content = QObject::tr(
+ "The program you are attempting to launch is blacklisted in the virtual "
+ "filesystem. This will likely prevent it from seeing any mods, INI files "
+ "or any other virtualized files.");
+
+ const auto details =
+ "Executable: " + sp.binary.fileName() + "\n"
+ "Current blacklist: " + settings.executablesBlacklist();
+
+ auto r = MOBase::TaskDialog(parent, title)
+ .main(mainText)
+ .content(content)
+ .details(details)
+ .icon(QMessageBox::Question)
+ .remember("blacklistedExecutable", sp.binary.fileName())
+ .button({
+ QObject::tr("Continue"),
+ QObject::tr("Your mods might not work."),
+ QMessageBox::Yes})
+ .button({
+ QObject::tr("Change the blacklist"),
+ QMessageBox::Retry})
+ .button({
+ QObject::tr("Cancel"),
+ QMessageBox::Cancel})
+ .exec();
+
+ if (r == QMessageBox::Retry) {
+ if (!WorkaroundsSettingsTab::changeBlacklistNow(parent, settings)) {
+ r = QMessageBox::Cancel;
+ }
+ }
+
+ return r;
+}
+
+} // namespace
+
+
+namespace spawn
+{
+
+void logSpawning(const SpawnParameters& sp, const QString& realCmd)
+{
+ log::debug(
+ "spawning binary:\n"
+ " . exe: '{}'\n"
+ " . args: '{}'\n"
+ " . cwd: '{}'\n"
+ " . steam id: '{}'\n"
+ " . hooked: {}\n"
+ " . stdout: {}\n"
+ " . stderr: {}\n"
+ " . real cmd: '{}'",
+ sp.binary.absoluteFilePath(), sp.arguments,
+ sp.currentDirectory.absolutePath(), sp.steamAppID, sp.hooked,
+ (sp.stdOut == INVALID_HANDLE_VALUE ? "no" : "yes"),
+ (sp.stdErr == INVALID_HANDLE_VALUE ? "no" : "yes"),
+ realCmd);
+}
+
+DWORD spawn(const SpawnParameters& sp, HANDLE& processHandle)
{
BOOL inheritHandles = FALSE;
- STARTUPINFO si;
- ::ZeroMemory(&si, sizeof(si));
- if (stdOut != INVALID_HANDLE_VALUE) {
- si.hStdOutput = stdOut;
+
+ STARTUPINFO si = {};
+ si.cb = sizeof(si);
+
+ // inherit handles if we plan to use stdout or stderr reroute
+ if (sp.stdOut != INVALID_HANDLE_VALUE) {
+ si.hStdOutput = sp.stdOut;
inheritHandles = TRUE;
si.dwFlags |= STARTF_USESTDHANDLES;
}
- if (stdErr != INVALID_HANDLE_VALUE) {
- si.hStdError = stdErr;
+
+ if (sp.stdErr != INVALID_HANDLE_VALUE) {
+ si.hStdError = sp.stdErr;
inheritHandles = TRUE;
si.dwFlags |= STARTF_USESTDHANDLES;
}
- si.cb = sizeof(si);
- size_t length = wcslen(binary) + wcslen(arguments) + 4;
- wchar_t *commandLine = nullptr;
- if (arguments[0] != L'\0') {
- commandLine = new wchar_t[length];
- _snwprintf(commandLine, length, L"\"%ls\" %ls", binary, arguments);
+
+ const auto bin = QDir::toNativeSeparators(sp.binary.absoluteFilePath());
+ const auto cwd = QDir::toNativeSeparators(sp.currentDirectory.absolutePath());
+
+ QString commandLine = "\"" + bin + "\"";
+ if (!sp.arguments.isEmpty()) {
+ commandLine += " " + sp.arguments;
+ }
+
+ const QString moPath = QCoreApplication::applicationDirPath();
+ const auto oldPath = env::addPath(QDir::toNativeSeparators(moPath));
+
+ PROCESS_INFORMATION pi = {};
+ BOOL success = FALSE;
+
+ logSpawning(sp, commandLine);
+
+ const auto wcommandLine = commandLine.toStdWString();
+ const auto wcwd = cwd.toStdWString();
+
+ const DWORD flags = CREATE_BREAKAWAY_FROM_JOB;
+
+ if (sp.hooked) {
+ success = ::CreateProcessHooked(
+ nullptr, const_cast<wchar_t*>(wcommandLine.c_str()), nullptr, nullptr,
+ inheritHandles, flags, nullptr, wcwd.c_str(), &si, &pi);
} else {
- commandLine = new wchar_t[length];
- _snwprintf_s(commandLine, length, _TRUNCATE, L"\"%ls\"", binary);
+ success = ::CreateProcess(
+ nullptr, const_cast<wchar_t*>(wcommandLine.c_str()), nullptr, nullptr,
+ inheritHandles, flags, nullptr, wcwd.c_str(), &si, &pi);
+ }
+
+ const auto e = GetLastError();
+ env::setPath(oldPath);
+
+ if (!success) {
+ return e;
}
- QString moPath = QCoreApplication::applicationDirPath();
+ processHandle = pi.hProcess;
+ ::CloseHandle(pi.hThread);
+
+ return ERROR_SUCCESS;
+}
- boost::scoped_array<TCHAR> oldPath(new TCHAR[BUFSIZE]);
- DWORD offset = ::GetEnvironmentVariable(TEXT("PATH"), oldPath.get(), BUFSIZE);
- if (offset > BUFSIZE) {
- oldPath.reset(new TCHAR[offset]);
- ::GetEnvironmentVariable(TEXT("PATH"), oldPath.get(), offset);
+bool restartAsAdmin(QWidget* parent)
+{
+ WCHAR cwd[MAX_PATH] = {};
+ if (!GetCurrentDirectory(MAX_PATH, cwd)) {
+ cwd[0] = L'\0';
}
+ if (!helper::adminLaunch(
+ parent,
+ qApp->applicationDirPath().toStdWString(),
+ qApp->applicationFilePath().toStdWString(),
+ std::wstring(cwd)))
{
- boost::scoped_array<TCHAR> newPath(new TCHAR[offset + moPath.length() + 2]);
- _tcsncpy(newPath.get(), oldPath.get(), offset);
- newPath.get()[offset] = '\0';
- _tcsncat(newPath.get(), TEXT(";"), 1);
- _tcsncat(newPath.get(), ToWString(QDir::toNativeSeparators(moPath)).c_str(), moPath.length());
+ log::error("admin launch failed");
+ return false;
+ }
- ::SetEnvironmentVariable(TEXT("PATH"), newPath.get());
+ log::debug("exiting MO");
+ ExitModOrganizer(Exit::Force);
+
+ return true;
+}
+
+void startBinaryAdmin(QWidget* parent, const SpawnParameters& sp)
+{
+ if (!dialogs::confirmRestartAsAdmin(parent, sp)) {
+ log::debug("user declined");
+ return;
}
- PROCESS_INFORMATION pi;
- BOOL success = FALSE;
- if (hooked) {
- success = ::CreateProcessHooked(nullptr,
- commandLine,
- nullptr, nullptr, // no special process or thread attributes
- inheritHandles, // inherit handles if we plan to use stdout or stderr reroute
- CREATE_BREAKAWAY_FROM_JOB,
- nullptr, // same environment as parent
- currentDirectory, // current directory
- &si, &pi // startup and process information
- );
+ log::info("restarting MO as administrator");
+ restartAsAdmin(parent);
+}
+
+struct SteamStatus
+{
+ bool running=false;
+ bool accessible=false;
+};
+
+SteamStatus getSteamStatus()
+{
+ SteamStatus ss;
+
+ const auto ps = env::Environment().runningProcesses();
+
+ for (const auto& p : ps) {
+ if ((p.name().compare("Steam.exe", Qt::CaseInsensitive) == 0) ||
+ (p.name().compare("SteamService.exe", Qt::CaseInsensitive) == 0))
+ {
+ ss.running = true;
+ ss.accessible = p.canAccess();
+
+ log::debug(
+ "'{}' is running, accessible={}",
+ p.name(), (ss.accessible ? "yes" : "no"));
+
+ break;
+ }
+ }
+
+ return ss;
+}
+
+QString makeSteamArguments(const QString& username, const QString& password)
+{
+ QString args;
+
+ if (username != "") {
+ args += "-login " + username;
+
+ if (password != "") {
+ args += " " + password;
+ }
+ }
+
+ return args;
+}
+
+bool startSteam(QWidget* parent)
+{
+ const QString keyName = "HKEY_CURRENT_USER\\Software\\Valve\\Steam";
+ const QString valueName = "SteamExe";
+
+ const QSettings steamSettings(keyName, QSettings::NativeFormat);
+ const QString exe = steamSettings.value(valueName, "").toString();
+
+ if (exe.isEmpty()) {
+ return (dialogs::badSteamReg(parent, keyName, valueName) == QMessageBox::Yes);
+ }
+
+ SpawnParameters sp;
+ sp.binary = exe;
+
+ // See if username and password supplied. If so, pass them into steam.
+ QString username, password;
+ if (Settings::instance().steam().login(username, password)) {
+ sp.arguments = makeSteamArguments(username, password);
+ }
+
+ log::debug(
+ "starting steam process:\n"
+ " . program: '{}'\n"
+ " . username={}, password={}",
+ sp.binary.filePath().toStdString(),
+ (username.isEmpty() ? "no" : "yes"),
+ (password.isEmpty() ? "no" : "yes"));
+
+ HANDLE ph = INVALID_HANDLE_VALUE;
+ const auto e = spawn(sp, ph);
+ ::CloseHandle(ph);
+
+ if (e != ERROR_SUCCESS) {
+ // make sure username and passwords are not shown
+ sp.arguments = makeSteamArguments(
+ (username.isEmpty() ? "" : "USERNAME"),
+ (password.isEmpty() ? "" : "PASSWORD"));
+
+ const auto r = dialogs::startSteamFailed(
+ parent, keyName, valueName, exe, sp, e);
+
+ return (r == QMessageBox::Yes);
+ }
+
+ QMessageBox::information(
+ parent, QObject::tr("Waiting"),
+ QObject::tr("Please press OK once you're logged into steam."));
+
+ return true;
+}
+
+bool checkSteam(
+ QWidget* parent, const SpawnParameters& sp,
+ const QDir& gameDirectory, const QString &steamAppID, const Settings& settings)
+{
+ static const std::vector<QString> steamFiles = {
+ "steam_api.dll", "steam_api64.dll"
+ };
+
+ log::debug("checking steam");
+
+ if (!steamAppID.isEmpty()) {
+ env::set("SteamAPPId", steamAppID);
} else {
- success = ::CreateProcess(nullptr,
- commandLine,
- nullptr, nullptr, // no special process or thread attributes
- inheritHandles, // inherit handles if we plan to use stdout or stderr reroute
- CREATE_BREAKAWAY_FROM_JOB,
- nullptr, // same environment as parent
- currentDirectory, // current directory
- &si, &pi // startup and process information
- );
+ env::set("SteamAPPId", settings.steam().appID());
}
- ::SetEnvironmentVariable(TEXT("PATH"), oldPath.get());
- delete [] commandLine;
+ bool steamRequired = false;
+ QString details;
- if (!success) {
- throw windows_error("failed to start process");
+ for (const auto& file : steamFiles) {
+ const QFileInfo fi(gameDirectory.absoluteFilePath(file));
+ if (fi.exists()) {
+ details = QString(
+ "managed game is located at '%1' and file '%2' exists")
+ .arg(gameDirectory.absolutePath())
+ .arg(fi.absoluteFilePath());
+
+ log::debug("{}", details);
+ steamRequired = true;
+
+ break;
+ }
+ }
+
+ if (!steamRequired) {
+ log::debug("program doesn't seem to require steam");
+ return true;
+ }
+
+
+ auto ss = getSteamStatus();
+
+ if (!ss.running) {
+ log::debug("steam isn't running, asking to start steam");
+
+ const auto c = dialogs::confirmStartSteam(parent, sp, details);
+
+ if (c == QDialogButtonBox::Yes) {
+ log::debug("user wants to start steam");
+
+ if (!startSteam(parent)) {
+ // cancel
+ return false;
+ }
+
+ // double-check that Steam is started
+ ss = getSteamStatus();
+ if (!ss.running) {
+ log::error("steam is still not running, hoping for the best");
+ return true;
+ }
+ } else if (c == QDialogButtonBox::No) {
+ log::debug("user declined to start steam");
+ return true;
+ } else {
+ log::debug("user cancelled");
+ return false;
+ }
+ }
+
+ if (ss.running && !ss.accessible) {
+ log::debug("steam is running but is not accessible, asking to restart MO");
+ const auto c = dialogs::confirmRestartAsAdminForSteam(parent, sp);
+
+ if (c == QDialogButtonBox::Yes) {
+ restartAsAdmin(parent);
+ return false;
+ } else if (c == QDialogButtonBox::No) {
+ log::debug("user declined to restart MO, continuing");
+ return true;
+ } else {
+ log::debug("user cancelled");
+ return false;
+ }
}
- processHandle = pi.hProcess;
- threadHandle = pi.hThread;
return true;
}
+bool checkBlacklist(
+ QWidget* parent, const SpawnParameters& sp, Settings& settings)
+{
+ for (;;) {
+ if (!settings.isExecutableBlacklisted(sp.binary.fileName())) {
+ return true;
+ }
+
+ const auto r = dialogs::confirmBlacklisted(parent, sp, settings);
-HANDLE startBinary(const QFileInfo &binary,
- const QString &arguments,
- const QDir &currentDirectory,
- bool hooked,
- HANDLE stdOut,
- HANDLE stdErr)
+ if (r != QMessageBox::Retry) {
+ return (r == QMessageBox::Yes);
+ }
+ }
+}
+
+HANDLE startBinary(QWidget* parent, const SpawnParameters& sp)
{
- HANDLE processHandle, threadHandle;
- std::wstring binaryName = ToWString(QDir::toNativeSeparators(binary.absoluteFilePath()));
- std::wstring currentDirectoryName = ToWString(QDir::toNativeSeparators(currentDirectory.absolutePath()));
+ HANDLE handle = INVALID_HANDLE_VALUE;
+ const auto e = spawn::spawn(sp, handle);
- try {
- if (!spawn(binaryName.c_str(), ToWString(arguments).c_str(), currentDirectoryName.c_str(),
- true, hooked, stdOut, stdErr, processHandle, threadHandle)) {
- reportError(QObject::tr("failed to spawn \"%1\"").arg(binary.fileName()));
- return INVALID_HANDLE_VALUE;
+ switch (e)
+ {
+ case ERROR_SUCCESS:
+ {
+ return handle;
}
- } catch (const windows_error &e) {
- if (e.getErrorCode() == ERROR_ELEVATION_REQUIRED) {
- if (QMessageBox::question(QApplication::activeModalWidget(), QObject::tr("Elevation required"),
- QObject::tr("This process requires elevation to run.\n"
- "This is a potential security risk so I highly advise you to investigate if\n"
- "\"%1\"\n"
- "can be installed to work without elevation.\n\n"
- "Restart Mod Organizer as an elevated process?\n"
- "You will be asked if you want to allow helper.exe to make changes to the system. "
- "You will need to relaunch the process above manually.").arg(
- QDir::toNativeSeparators(binary.absoluteFilePath())),
- QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) {
- WCHAR cwd[MAX_PATH];
- if (!GetCurrentDirectory(MAX_PATH, cwd)) {
- reportError(QObject::tr("failed to spawn \"%1\": %2").arg(binary.fileName()).arg(::GetLastError()));
- cwd[0] = L'\0';
- }
- if (!Helper::adminLaunch(
- qApp->applicationDirPath().toStdWString(),
- qApp->applicationFilePath().toStdWString(),
- std::wstring(cwd))) {
- return INVALID_HANDLE_VALUE;
- }
- qApp->exit(0);
- }
+
+ case ERROR_ELEVATION_REQUIRED:
+ {
+ startBinaryAdmin(parent, sp);
return INVALID_HANDLE_VALUE;
+ }
- } else {
- reportError(QObject::tr("failed to spawn \"%1\": %2").arg(binary.fileName()).arg(e.what()));
+ default:
+ {
+ dialogs::spawnFailed(parent, sp, e);
return INVALID_HANDLE_VALUE;
}
}
+}
+
+QString getExecutableForJarFile(const QString& jarFile)
+{
+ const std::wstring jarFileW = jarFile.toStdWString();
+
+ WCHAR buffer[MAX_PATH];
+
+ const auto hinst = ::FindExecutableW(jarFileW.c_str(), nullptr, buffer);
+ const auto r = static_cast<int>(reinterpret_cast<std::uintptr_t>(hinst));
+
+ // anything <= 32 signals failure
+ if (r <= 32) {
+ log::warn(
+ "failed to find executable associated with file '{}', {}",
+ jarFile, shell::formatError(r));
+
+ return {};
+ }
+
+ DWORD binaryType = 0;
+
+ if (!::GetBinaryTypeW(buffer, &binaryType)) {
+ const auto e = ::GetLastError();
+
+ log::warn(
+ "failed to determine binary type of '{}', {}",
+ QString::fromWCharArray(buffer), formatSystemMessage(e));
+
+ return {};
+ }
+
+ if (binaryType != SCS_32BIT_BINARY && binaryType != SCS_64BIT_BINARY) {
+ log::warn(
+ "unexpected binary type {} for file '{}'",
+ binaryType, QString::fromWCharArray(buffer));
+
+ return {};
+ }
+
+ return QString::fromWCharArray(buffer);
+}
+
+QString getJavaHome()
+{
+ const QString key = "HKEY_LOCAL_MACHINE\\Software\\JavaSoft\\Java Runtime Environment";
+ const QString value = "CurrentVersion";
+
+ QSettings reg(key, QSettings::NativeFormat);
+
+ if (!reg.contains(value)) {
+ log::warn("key '{}\\{}' doesn't exist", key, value);
+ return {};
+ }
+
+ const QString currentVersion = reg.value("CurrentVersion").toString();
+ const QString javaHome = QString("%1/JavaHome").arg(currentVersion);
+
+ if (!reg.contains(javaHome)) {
+ log::warn(
+ "java version '{}' was found at '{}\\{}', but '{}\\{}' doesn't exist",
+ currentVersion, key, value, key, javaHome);
+
+ return {};
+ }
+
+ const auto path = reg.value(javaHome).toString();
+ return path + "\\bin\\javaw.exe";
+}
+
+QString findJavaInstallation(const QString& jarFile)
+{
+ // try to find java automatically based on the given jar file
+ if (!jarFile.isEmpty()) {
+ const auto s = getExecutableForJarFile(jarFile);
+ if (!s.isEmpty()) {
+ return s;
+ }
+ }
+
+ // second attempt: look to the registry
+ const auto s = getJavaHome();
+ if (!s.isEmpty()) {
+ return s;
+ }
+
+ // not found
+ return {};
+}
+
+bool isBatchFile(const QFileInfo& target)
+{
+ const auto batchExtensions = {"cmd", "bat"};
+
+ const QString extension = target.suffix();
+ for (auto&& e : batchExtensions) {
+ if (extension.compare(e, Qt::CaseInsensitive) == 0) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+bool isExeFile(const QFileInfo& target)
+{
+ return (target.suffix().compare("exe", Qt::CaseInsensitive) == 0);
+}
+
+bool isJavaFile(const QFileInfo& target)
+{
+ return (target.suffix().compare("jar", Qt::CaseInsensitive) == 0);
+}
+
+QFileInfo getCmdPath()
+{
+ const auto p = env::get("COMSPEC");
+ if (!p.isEmpty()) {
+ return p;
+ }
+
+ QString systemDirectory;
+
+ const std::size_t buffer_size = 1000;
+ wchar_t buffer[buffer_size + 1] = {};
+
+ const auto length = ::GetSystemDirectoryW(buffer, buffer_size);
+ if (length != 0) {
+ systemDirectory = QString::fromWCharArray(buffer, length);
+
+ if (!systemDirectory.endsWith("\\")) {
+ systemDirectory += "\\";
+ }
+ } else {
+ systemDirectory = "C:\\Windows\\System32\\";
+ }
+
+ return systemDirectory + "cmd.exe";
+}
+
+FileExecutionContext getFileExecutionContext(
+ QWidget* parent, const QFileInfo& target)
+{
+ if (isExeFile(target)) {
+ return {
+ target,
+ "",
+ FileExecutionTypes::Executable
+ };
+ }
+
+ if (isBatchFile(target)) {
+ return {
+ getCmdPath(),
+ QString("/C \"%1\"").arg(QDir::toNativeSeparators(target.absoluteFilePath())),
+ FileExecutionTypes::Executable
+ };
+ }
+
+ if (isJavaFile(target)) {
+ auto java = findJavaInstallation(target.absoluteFilePath());
+
+ if (java.isEmpty()) {
+ java = QFileDialog::getOpenFileName(
+ parent, QObject::tr("Select binary"),
+ QString(), QObject::tr("Binary") + " (*.exe)");
+ }
+
+ if (!java.isEmpty()) {
+ return {
+ QFileInfo(java),
+ QString("-jar \"%1\"").arg(QDir::toNativeSeparators(target.absoluteFilePath())),
+ FileExecutionTypes::Executable
+ };
+ }
+ }
- ::CloseHandle(threadHandle);
- return processHandle;
+ return {{}, {}, FileExecutionTypes::Other};
}
+
+} // namespace
+
+
+
+namespace helper
+{
+
+bool helperExec(
+ QWidget* parent,
+ const std::wstring& moDirectory, const std::wstring& commandLine, BOOL async)
+{
+ const std::wstring fileName = moDirectory + L"\\helper.exe";
+
+ env::HandlePtr process;
+
+ {
+ SHELLEXECUTEINFOW execInfo = {};
+
+ ULONG flags = SEE_MASK_FLAG_NO_UI;
+ if (!async)
+ flags |= SEE_MASK_NOCLOSEPROCESS;
+
+ execInfo.cbSize = sizeof(SHELLEXECUTEINFOW);
+ execInfo.fMask = flags;
+ execInfo.hwnd = 0;
+ execInfo.lpVerb = L"runas";
+ execInfo.lpFile = fileName.c_str();
+ execInfo.lpParameters = commandLine.c_str();
+ execInfo.lpDirectory = moDirectory.c_str();
+ execInfo.nShow = SW_SHOW;
+
+ if (!::ShellExecuteExW(&execInfo) && execInfo.hProcess == 0) {
+ const auto e = GetLastError();
+
+ spawn::dialogs::helperFailed(
+ parent, e, "ShellExecuteExW()", fileName, moDirectory, commandLine);
+
+ return false;
+ }
+
+ if (async) {
+ return true;
+ }
+
+ process.reset(execInfo.hProcess);
+ }
+
+ const auto r = ::WaitForSingleObject(process.get(), INFINITE);
+
+ if (r != WAIT_OBJECT_0) {
+ // for WAIT_ABANDONED, the documentation doesn't mention that GetLastError()
+ // returns something meaningful, but code ERROR_ABANDONED_WAIT_0 exists, so
+ // use that instead
+ const auto code = (r == WAIT_ABANDONED ?
+ ERROR_ABANDONED_WAIT_0 : GetLastError());
+
+ spawn::dialogs::helperFailed(
+ parent, code, "WaitForSingleObject()",
+ fileName, moDirectory, commandLine);
+
+ return false;
+ }
+
+ DWORD exitCode = 0;
+ if (!GetExitCodeProcess(process.get(), &exitCode)) {
+ const auto e = GetLastError();
+
+ spawn::dialogs::helperFailed(
+ parent, e, "GetExitCodeProcess()", fileName, moDirectory, commandLine);
+
+ return false;
+ }
+
+ return (exitCode == 0);
+}
+
+bool backdateBSAs(
+ QWidget* parent, const std::wstring &moPath, const std::wstring &dataPath)
+{
+ const std::wstring commandLine = fmt::format(
+ L"backdateBSA \"{}\"", dataPath);
+
+ return helperExec(parent, moPath, commandLine, FALSE);
+}
+
+bool adminLaunch(
+ QWidget* parent, const std::wstring &moPath,
+ const std::wstring &moFile, const std::wstring &workingDir)
+{
+ const std::wstring commandLine = fmt::format(
+ L"adminLaunch {} \"{}\" \"{}\"",
+ ::GetCurrentProcessId(), moFile, workingDir);
+
+ return helperExec(parent, moPath, commandLine, true);
+}
+
+} // namespace
diff --git a/src/spawn.h b/src/spawn.h
index c2d99bdb..a615b5ff 100644
--- a/src/spawn.h
+++ b/src/spawn.h
@@ -26,28 +26,12 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#include <QFileInfo>
#include <QDir>
+class Settings;
-/**
- * @brief a dirty little trick so we can issue a clean restart from startBinary
- * @note unused
- */
-/*class ExitProxy : public QObject {
- Q_OBJECT
-public:
- static ExitProxy *instance();
- void emitExit();
-signals:
- void exit();
-private:
- ExitProxy() {}
-private:
- static ExitProxy *s_Instance;
-};*/
-
+namespace spawn
+{
-/**
- * @brief spawn a binary with Mod Organizer injected
- *
+/*
* @param binary the binary to spawn
* @param arguments arguments to pass to the binary
* @param profileName name of the active profile
@@ -56,13 +40,80 @@ private:
* @param hooked if set, the binary is started with mo injected
* @param stdout if not equal to INVALID_HANDLE_VALUE, this is used as stdout for the process
* @param stderr if not equal to INVALID_HANDLE_VALUE, this is used as stderr for the process
+*/
+struct SpawnParameters
+{
+ QFileInfo binary;
+ QString arguments;
+ QDir currentDirectory;
+ QString steamAppID;
+ bool hooked = false;
+ HANDLE stdOut = INVALID_HANDLE_VALUE;
+ HANDLE stdErr = INVALID_HANDLE_VALUE;
+};
+
+
+bool checkSteam(
+ QWidget* parent, const SpawnParameters& sp,
+ const QDir& gameDirectory, const QString &steamAppID, const Settings& settings);
+
+bool checkBlacklist(
+ QWidget* parent, const SpawnParameters& sp, Settings& settings);
+
+/**
+ * @brief spawn a binary with Mod Organizer injected
* @return the process handle
- * @todo is the profile name even used any more?
- * @todo is the hooked parameter used?
**/
-HANDLE startBinary(const QFileInfo &binary, const QString &arguments,
- const QDir &currentDirectory, bool hooked,
- HANDLE stdOut = INVALID_HANDLE_VALUE, HANDLE stdErr = INVALID_HANDLE_VALUE);
+HANDLE startBinary(QWidget* parent, const SpawnParameters& sp);
+
+
+enum class FileExecutionTypes
+{
+ Executable = 1,
+ Other
+};
+
+struct FileExecutionContext
+{
+ QFileInfo binary;
+ QString arguments;
+ FileExecutionTypes type;
+};
+
+QString findJavaInstallation(const QString& jarFile);
+
+FileExecutionContext getFileExecutionContext(
+ QWidget* parent, const QFileInfo& target);
+
+} // namespace
+
+
+// convenience functions to work with the external helper program, which is used
+// to make changes on the system that require administrative rights, so that
+// ModOrganizer itself can run without special privileges
+//
+namespace helper
+{
+
+/**
+* @brief sets the last modified time for all .bsa-files in the target directory well into the past
+* @param moPath absolute path to the modOrganizer base directory
+* @param dataPath the path taht contains the .bsa-files, usually the data directory of the game
+**/
+bool backdateBSAs(
+ QWidget* parent, const std::wstring &moPath, const std::wstring &dataPath);
+
+/**
+* @brief waits for the current process to exit and restarts it as an administrator
+* @param moPath absolute path to the modOrganizer base directory
+* @param moFile file name of modOrganizer
+* @param workingDir current working directory
+**/
+bool adminLaunch(
+ QWidget* parent, const std::wstring &moPath,
+ const std::wstring &moFile, const std::wstring &workingDir);
+
+} // namespace
#endif // SPAWN_H
diff --git a/src/statusbar.cpp b/src/statusbar.cpp
index e9a6e658..4729c6ad 100644
--- a/src/statusbar.cpp
+++ b/src/statusbar.cpp
@@ -3,26 +3,32 @@
#include "settings.h"
#include "ui_mainwindow.h"
-StatusBar::StatusBar(QStatusBar* bar, Ui::MainWindow* ui) :
- m_bar(bar), m_progress(new QProgressBar),
- m_notifications(new StatusBarAction(ui->actionNotifications)),
- m_update(new StatusBarAction(ui->actionUpdate)),
- m_api(new QLabel)
+StatusBar::StatusBar(QWidget* parent) :
+ QStatusBar(parent), ui(nullptr), m_progress(new QProgressBar),
+ m_notifications(nullptr), m_update(nullptr), m_api(new QLabel)
{
+}
+
+void StatusBar::setup(Ui::MainWindow* mainWindowUI, const Settings& settings)
+{
+ ui = mainWindowUI;
+ m_notifications = new StatusBarAction(ui->actionNotifications);
+ m_update = new StatusBarAction(ui->actionUpdate);
+
QWidget* spacer1 = new QWidget;
spacer1->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
spacer1->setHidden(true);
spacer1->setVisible(true);
- m_bar->addPermanentWidget(spacer1, 0);
- m_bar->addPermanentWidget(m_progress);
+ addPermanentWidget(spacer1, 0);
+ addPermanentWidget(m_progress);
QWidget* spacer2 = new QWidget;
spacer2->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
spacer2->setHidden(true);
spacer2->setVisible(true);
- m_bar->addPermanentWidget(spacer2,0);
- m_bar->addPermanentWidget(m_notifications);
- m_bar->addPermanentWidget(m_update);
- m_bar->addPermanentWidget(m_api);
+ addPermanentWidget(spacer2,0);
+ addPermanentWidget(m_notifications);
+ addPermanentWidget(m_update);
+ addPermanentWidget(m_api);
m_progress->setTextVisible(true);
@@ -42,18 +48,20 @@ StatusBar::StatusBar(QStatusBar* bar, Ui::MainWindow* ui) :
"be unable to queue downloads, check updates, parse mod info, or even log "
"in. Both pools must be consumed before this happens."));
- m_bar->clearMessage();
+ clearMessage();
setProgress(-1);
setAPI({}, {});
+
+ checkSettings(settings);
}
void StatusBar::setProgress(int percent)
{
if (percent < 0 || percent >= 100) {
- m_bar->clearMessage();
+ clearMessage();
m_progress->setVisible(false);
} else {
- m_bar->showMessage(QObject::tr("Loading..."));
+ showMessage(QObject::tr("Loading..."));
m_progress->setVisible(true);
m_progress->setValue(percent);
}
@@ -61,7 +69,9 @@ void StatusBar::setProgress(int percent)
void StatusBar::setNotifications(bool hasNotifications)
{
- m_notifications->set(hasNotifications);
+ if (m_notifications) {
+ m_notifications->set(hasNotifications);
+ }
}
void StatusBar::setAPI(const APIStats& stats, const APIUserAccount& user)
@@ -123,7 +133,38 @@ void StatusBar::setUpdateAvailable(bool b)
void StatusBar::checkSettings(const Settings& settings)
{
- m_api->setVisible(!settings.hideAPICounter());
+ m_api->setVisible(!settings.interface().hideAPICounter());
+}
+
+void StatusBar::showEvent(QShowEvent*)
+{
+ visibilityChanged(true);
+}
+
+void StatusBar::hideEvent(QHideEvent*)
+{
+ visibilityChanged(false);
+}
+
+void StatusBar::visibilityChanged(bool visible)
+{
+ // the central widget typically has no bottom padding because the status bar
+ // is more than enough, but when it's hidden, the bottom widget (currently
+ // the log) touches the bottom border of the window, which looks ugly
+ //
+ // when hiding the statusbar, the central widget is given the same border
+ // margin as it has on the top (which is typically 6, as it's the default from
+ // the qt designer)
+
+ auto m = ui->centralWidget->layout()->contentsMargins();
+
+ if (visible) {
+ m.setBottom(0);
+ } else {
+ m.setBottom(m.top());
+ }
+
+ ui->centralWidget->layout()->setContentsMargins(m);
}
diff --git a/src/statusbar.h b/src/statusbar.h
index 2baf12ee..3da45d41 100644
--- a/src/statusbar.h
+++ b/src/statusbar.h
@@ -29,10 +29,12 @@ private:
};
-class StatusBar
+class StatusBar : public QStatusBar
{
public:
- StatusBar(QStatusBar* bar, Ui::MainWindow* ui);
+ StatusBar(QWidget* parent=nullptr);
+
+ void setup(Ui::MainWindow* ui, const Settings& settings);
void setProgress(int percent);
void setNotifications(bool hasNotifications);
@@ -40,12 +42,18 @@ public:
void setUpdateAvailable(bool b);
void checkSettings(const Settings& settings);
+protected:
+ void showEvent(QShowEvent* e);
+ void hideEvent(QHideEvent* e);
+
private:
- QStatusBar* m_bar;
+ Ui::MainWindow* ui;
QProgressBar* m_progress;
StatusBarAction* m_notifications;
StatusBarAction* m_update;
QLabel* m_api;
+
+ void visibilityChanged(bool visible);
};
#endif // MO_STATUSBAR_H
diff --git a/src/stylesheets/Night Eyes.qss b/src/stylesheets/Night Eyes.qss
index 142acb6a..14f7434e 100644
--- a/src/stylesheets/Night Eyes.qss
+++ b/src/stylesheets/Night Eyes.qss
@@ -49,6 +49,10 @@ QAbstractScrollArea::corner
margin: 0px -2px -2px 0px;
}
+LinkLabel
+{
+ qproperty-linkColor: #3399FF;
+}
/* Toolbar -------------------------------------------------------------------- */
@@ -80,6 +84,11 @@ QToolButton:pressed
background: #181818;
}
+QToolButton::menu-indicator
+{
+ width: 8px;
+}
+
/* Left Pane & File Trees ----------------------------------------------------- */
diff --git a/src/stylesheets/Paper Automata.qss b/src/stylesheets/Paper Automata.qss
deleted file mode 100644
index 7c0a604e..00000000
--- a/src/stylesheets/Paper Automata.qss
+++ /dev/null
@@ -1,1002 +0,0 @@
-/* v2.0 Paper Automata by 6788-00 */
-/* https://6788-00.tumblr.com/ */
-
-/* Color Palette */
-/* Background - Main | #CDC8B0 */
-/* Background - Content | #DAD4BB */
-/* Hover | #B4AF9A */
-/* Selected | #4E4B42 */
-/* Accent | #CD664D */
-
-/* All */
-
-* {
- color: #4E4B42;
- font-size: 13px;
-}
-
-/* Main Window */
-
-QMainWindow,
-QDialog {
- /* most windows */
- background: url(./Paper/Automata/background.svg);
-}
-
-QWidget:disabled {
- /* disabled parts of the window like the update button when there are no updates */
- background: url(./Paper/Automata/background.svg);
- color: #B4AF9A;
-}
-
-QSplitter {
- /* for resizing the left pane, right pane, or log console */
- height: 2px;
- width: 8px;
-}
-
-QSplitter::handle:horizontal {
- /* horizontal handle */
- background: transparent;
-}
-
-QSplitter::handle {
- /* actual visible handle */
- background: #4E4B42;
- margin-top: 6px;
-}
-
-QAbstractItemView {
- /* left and right pane container */
- background: #DAD4BB;
- alternate-background-color: transparent;
- show-decoration-selected: 1;
- selection-background-color: #B4AF9A;
- selection-color: #4E4B42;
-}
-
-QAbstractItemView::item {
- min-height: 22px;
-}
-
-QAbstractItemView::item:hover {
- /* rows on left and right pane when moused-over */
- background: #B4AF9A;
-}
-
-QAbstractItemView::item:selected {
- /* rows on left and right pane when clicked */
- background: #4E4B42;
- color: #CDC8B0;
-}
-
-QAbstractScrollArea::corner {
- /* corner between where a vertical scrollbar and a horizontal scrollbar meet */
- background: #4E4B42;
- border: 2px solid #4E4B42;
- margin: 0px -2px -2px 0px;
-}
-
-/* Toolbar */
-
-QToolBar {
- /* top toolbar; */
- border-bottom: 2px solid #4E4B42;
- margin-left: 12px;
- margin-right: 12px;
-}
-
-QToolBar QWidget {
- background: transparent;
- margin: 0px;
-}
-
-QToolButton {
- /* toolbar buttons */
- padding: 4px 6px;
- border: 2px solid transparent;
- margin: 4px 4px 0px 4px;
-}
-
-QToolButton:hover,
-QToolButton:pressed {
- background: #B4AF9A;
- border-bottom: 2px solid #4E4B42;
-}
-
-QToolButton:menu-indicator {
- /* expandable indicator on toolbar buttons */
- image: url(./Paper/Automata/Arrows/down.svg);
- margin: 4px;
-}
-
-QToolButton QMenu {
- /* toolbar button dropdown menus */
- margin: 0px;
-}
-
-/* Toolbar Button Icons */
-
-#actionChange_Game {
- qproperty-icon: url(./Paper/Automata/Toolbar/instances.svg);
-}
-
-#actionInstallMod {
- qproperty-icon: url(./Paper/Automata/Toolbar/archives.svg);
-}
-
-#actionNexus {
- qproperty-icon: url(./Paper/Automata/Toolbar/nexus.svg);
-}
-
-#actionAdd_Profile {
- qproperty-icon: url(./Paper/Automata/Toolbar/profiles.svg);
-}
-
-#actionModify_Executables {
- qproperty-icon: url(./Paper/Automata/Toolbar/executables.svg);
-}
-
-#actionTool {
- qproperty-icon: url(./Paper/Automata/Toolbar/tools.svg);
-}
-
-#actionSettings {
- qproperty-icon: url(./Paper/Automata/Toolbar/settings.svg);
-}
-
-#actionNotifications {
- qproperty-icon: url(./Paper/Automata/Toolbar/problems.svg);
-}
-
-#actionUpdate {
- qproperty-icon: url(./Paper/Automata/Toolbar/update.svg);
-}
-
-#actionHelp {
- qproperty-icon: url(./Paper/Automata/Toolbar/help.svg);
-}
-
-/* Left Pane & File Trees */
-
-QTreeView {
- /* left pane and right pane under QAbstractItemView*/
- border: none;
-}
-
-QTreeView::branch:has-siblings:!adjoins-item{
- background: none;
-}
-
-QTreeView::branch:closed:has-children:has-siblings,
-QTreeView::branch:closed:has-children:!has-siblings {
- /* a branch that is closed */
- image: url(./Paper/Automata/collapsed.svg) center no-repeat;
-}
-
-QTreeView::branch:open:has-children:has-siblings,
-QTreeView::branch:open:has-children:!has-siblings {
- /* a branch that is open */
- image: url(./Paper/Automata/expanded.svg) center no-repeat;
-}
-
-QTreeView::branch:hover {
- background: #B4AF9A;
-}
-
-QTreeView::branch:selected {
- /* rows on the left pane when clicked (below QAbstractItemView, i.e. to the left of the checkbox) */
- background: #4E4B42;
- color: #CDC8B0;
-}
-
-QTreeView::item:selected {
- /* entry on left pane when clicked */
- background: #4E4B42;
- color: #CDC8B0;
-}
-
-QListView {
- /* saves window */
- border: none;
-}
-
-QListView::item:hover {
- /* uncertain, assumed: rows on the saves window when moused-over */
- background: #B4AF9A;
-}
-
-QListView::item:selected {
- /*uncertain, assumed: rows on the saves window when clicked */
- background: #4E4B42;
- color: #FFFFFF;
-}
-
-QTextEdit {
- /* large text fields */
- background: #DAD4BB;
- border: none;
-}
-
-QWebView {
- /* Nexus Info window */
- background: #DAD4BB;
- border-radius: 0px;
-}
-
-/* Group Boxes */
-
-QGroupBox {
- /* boxes that group multiple elements together (e.g. on Settings) */
- padding: 24px 4px;
- border: none;
-}
-
-QGroupBox::title {
- /* title of group boxes */
- background: transparent;
- subcontrol-origin: padding;
- subcontrol-position: top left;
- padding: 4px 8px;
-}
-
-/* Text Fields */
-
-QLineEdit {
- /* text fields like NameFilter and directory fields */
- background: #DAD4BB;
- min-height: 14px;
- padding: 2px;
- border: 2px solid #DAD4BB;
- border-radius: 0px;
-}
-
-QLineEdit:hover {
- /* text fields when moused-over */
- background: 2px solid #B4AF9A;
- border: 2px solid #B4AF9A;
-}
-
-/* Most Dropdown Menus */
-
-QComboBox {
- /* dropdown menus */
- background: #DAD4BB;
- min-height: 20px;
- padding-left: 5px;
- border: 2px solid #DAD4BB;
- border-radius: 0px;
- margin: 4px 0px;
-}
-
-QComboBox:hover {
- /* dropdown menus when moused-over */
- background: 2px solid #B4AF9A;
- border: 2px solid #B4AF9A;
-}
-
-QComboBox:on {
- /* dropdown menus when expanded */
- background: #4E4B42;
- color: #DAD4BB;
- border: 2px solid #4E4B42;
-}
-
-QComboBox::drop-down {
- /* area for expandable indicator */
- width: 20px;
- subcontrol-origin: padding;
- subcontrol-position: top right;
- border: none;
-}
-
-QComboBox QAbstractItemView {
- /* actual menu that expands */
- background: #DAD4BB;
- border: 2px solid #CDC8B0;
- border-radius: 0px;
-}
-
-QComboBox::down-arrow {
- /* expandable indicator */
- image: url(./Paper/Automata/Arrows/down.svg);
-}
-
-/* Most Buttons */
-
-QPushButton {
- /* most buttons */
- background: #B4AF9A;
- min-height: 20px;
- padding: 2px 12px;
- border-radius: 0px;
-}
-
-QPushButton:disabled {
- /* most buttons when disabled */
- background: transparent;
- border: 2px solid #DAD4BB;
-}
-
-QPushButton:hover {
- /* most buttons when hovered */
- background: #DAD4BB;
-}
-
-QPushButton:pressed {
- /* most buttons when clicked */
- background: #4E4B42;
- color: #DAD4BB;
-}
-
-QPushButton::menu-indicator {
- /* expandable indicator for most buttons */
- subcontrol-position: right center;
- image: url(./Paper/Automata/Arrows/down.svg);
- padding: 2px;
- margin: 4px 4px;
-}
-
-/* Icons */
-
-#listOptionsBtn {
- /* Options button */
- qproperty-icon: url(./Paper/Automata/dots.svg);
- qproperty-iconSize: 16px;
- padding-left: 2px;
-}
-
-#openFolderMenu {
- /* Open Folder button */
- qproperty-icon: url(./Paper/Automata/folder.svg);
- qproperty-iconSize: 14px;
- padding-left: 4px;
-}
-
-#restoreModsButton,
-#restoreButton {
- /* Restore Backup buttons */
- qproperty-icon: url(./Paper/Automata/restore.svg);
- qproperty-iconSize: 14px;
-}
-
-#saveModsButton,
-#saveButton {
- /* Backup buttons */
- qproperty-icon: url(./Paper/Automata/backup.svg);
- qproperty-iconSize: 14px;
-}
-
-#bossButton {
- /* Sort button */
- qproperty-icon: url(./Paper/Automata/sort.svg);
- qproperty-iconSize: 14px;
-}
-
-#linkButton {
- /* Shortcuts button */
- qproperty-icon: url(./Paper/Automata/shortcut.svg);
- qproperty-iconSize: 14px;
-}
-
-#btnRefreshData,
-#refreshButton {
- /* Refresh buttons */
- qproperty-icon: url(./Paper/Automata/refresh.svg);
- qproperty-iconSize: 14px;
-}
-
-#endorseBtn {
- /* Endorse button on the Nexus Info tab of the Information window */
- qproperty-icon: url(./Paper/Automata/heart.svg);
- qproperty-iconSize: 14px;
-}
-
-#clearCacheButton {
- /* Clear Cache button on the Nexus tab of the Settings window */
- qproperty-icon: url(./Paper/Automata/cross.svg);
- qproperty-iconSize: 14px;
-}
-
-#deactivateESP,
-#activateESP {
- /* activate and deactivate ESP buttons */
- background: #DAD4BB;
-}
-
-#deactivateESP:hover,
-#activateESP:hover {
- /* activate and deactivate ESP buttons when moused-over */
- background: #B4AF9A;
-}
-
-#deactivateESP {
- /* icon for the deactivate ESP button */
- qproperty-icon: url(./Paper/Automata/backup.svg);
-}
-
-#activateESP {
- /* icon for the activate ESP button */
- qproperty-icon: url(./Paper/Automata/restore-alt.svg);
-}
-
-/* Run button */
-
-#startButton {
- /* Run button */
- background: #4E4B42;
- color: #DAD4BB;
- qproperty-icon: url(./Paper/Automata/run.svg);
- qproperty-iconSize: 30px;
- padding: 6px;
-}
-
-#startButton:hover {
- /* Run button when moused-over*/
- background: #B4AF9A;
- color: #4E4B42;
-}
-
-/* Scroll Bars */
-
-/* Horizontal */
-
-QScrollBar:horizontal {
- /* horizontal scroll bar */
- background: #DAD4BB;
- height: 18px;
- border: 2px solid #DAD4BB;
- margin: 0px 23px -2px 23px;
-}
-
-QScrollBar::handle:horizontal {
- /* handle for horizontal scroll bars */
- background: #4E4B42;
- min-width: 32px;
- margin: 2px;
-}
-
-QScrollBar::add-line:horizontal {
- /* scroll right button */
- background: #DAD4BB;
- image: url(./Paper/Automata/Arrows/right.svg);
- width: 23px;
- subcontrol-position: right;
- subcontrol-origin: margin;
- border: 2px solid #CDC8B0;
- margin: 0px -2px -2px 0px;
-}
-
-QScrollBar::sub-line:horizontal {
- /* scroll left button */
- background: #DAD4BB;
- image: url(./Paper/Automata/Arrows/left.svg);
- width: 23px;
- subcontrol-position: left;
- subcontrol-origin: margin;
- border: 2px solid #CDC8B0;
- margin: 0px 0px -2px -2px;
-}
-
-/* Vertical */
-
-QScrollBar:vertical {
- /* vertical scroll bar */
- background: #DAD4BB;
- width: 18px;
- border: 2px solid #CDC8B0;
- margin: 23px -2px 23px 0px;
-}
-
-QScrollBar::handle:vertical {
- /* handle for vertical scroll bars */
- background: #4E4B42;
- min-height: 32px;
- margin: 2px;
-}
-
-QScrollBar::add-line:vertical {
- /* scroll down button */
- background: #DAD4BB;
- image: url(./Paper/Automata/Arrows/down.svg);
- height: 23px;
- subcontrol-position: bottom;
- subcontrol-origin: margin;
- border: 2px solid #CDC8B0;
- border-bottom-right-radius: 0px;
- margin: 0px -2px -2px 0px;
-}
-
-QScrollBar::sub-line:vertical {
- /* scroll up button */
- background: #B4AF9A;
- image: url(./Paper/Automata/Arrows/up.svg);
- height: 23px;
- subcontrol-position: top;
- subcontrol-origin: margin;
- border: 2px solid #CDC8B0;
- border-top-right-radius: 0px;
- margin: -2px -2px 0px 0px;
-}
-
-/* Combined */
-
-QScrollBar::handle:horizontal:hover,
-QScrollBar::handle:vertical:hover,
-QScrollBar::add-line:horizontal:hover,
-QScrollBar::sub-line:horizontal:hover,
-QScrollBar::add-line:vertical:hover,
-QScrollBar::sub-line:vertical:hover {
- /* buttons and handles when moused-over */
- background: #B4AF9A;
-}
-
-QScrollBar::handle:horizontal:pressed,
-QScrollBar::handle:vertical:pressed,
-QScrollBar::add-line:horizontal:pressed,
-QScrollBar::sub-line:horizontal:pressed,
-QScrollBar::add-line:vertical:pressed,
-QScrollBar::sub-line:vertical:pressed {
- /* buttons and handles when clicked */
- background: #4E4B42;
-}
-
-QScrollBar::add-page:horizontal,
-QScrollBar::sub-page:horizontal,
-QScrollBar::add-page:vertical,
-QScrollBar::sub-page:vertical {
- /* area on scroll bars where clicking it scrolls to where you clicked */
- background: transparent;
-}
-
-/* Header Rows */
-
-QHeaderView {
- /* header row (i.e. Mod Name, Flags, Category, etc.) */
- background: #B4AF9A;
-}
-
-QHeaderView::section {
- /* each section on the header row (i.e. Mod name is one section and Flags another) */
- background: #B4AF9A;
- height: 22px;
- padding: 0px 5px;
- border: 0px;
- border-bottom: 2px solid #CDC8B0;
- border-right: 2px solid #CDC8B0;
-}
-
-QHeaderView::section:hover {
- /* a section on a header row when hovered */
- background: #B4AF9A;
-}
-
-QHeaderView::up-arrow {
- /* ascending sort indicator */
- image: url(./Paper/Automata/Arrows/up.svg);
- padding-right: 4px;
- height: 10px;
- width: 10px;
-}
-
-QHeaderView::down-arrow {
- /* descending sort indicator */
- image: url(./Paper/Automata/Arrows/down.svg);
- padding-right: 4px;
- height: 10px;
- width: 10px;
-}
-
-QHeaderView::section:last {
- margin-right: -2px;
-}
-
-/* Right Click Menus, Toolbar Dropdown Menus, & Tooltips */
-
-QMenuBar {
- background: #DAD4BB;
- border: 2px solid #CDC8B0;
-}
-
-QMenuBar::item:selected {
- background: #B4AF9A;
- border: none;
-}
-
-QMenu {
- /* right click menu */
- background: #DAD4BB;
- border: 2px solid #CDC8B0;
-}
-
-QMenu::item {
- /* rows on right click menus */
- background: #DAD4BB;
- padding: 5px 20px 5px 24px;
-}
-
-QMenu::item:selected {
- /* rows on right click menus when moused-over (i dunno) */
- background: #B4AF9A;
- border: none;
-}
-
-QMenu::item:disabled {
- /* unavailable rows on right click menus */
- background: #CDC8B0;
- color: #B4AF9A;
-}
-
-QMenu::separator {
- /* seperators on right click menus */
- height: 2px;
- background: #CDC8B0;
-}
-
-QMenu::icon {
- /* area for icons on right click menus */
- padding: 4px;
-}
-
-QMenu::right-arrow {
- /* submenu indicator */
- image: url(./Paper/Automata/Arrows/right.svg);
- padding-right: 5px;
-}
-
-QMenu QPushButton {
- /* Change Categories and Primary Categories buttons */
- background: #DAD4BB;
- padding: 2px 24px;
- text-align: left;
- border: none;
-}
-
-QMenu QPushButton:hover {
- /* Change Categories and Primary Categories buttons when moused-over */
- background: #B4AF9A;
- border: none;
-}
-
-QMenu QCheckBox {
- /* checkboxes on right click menus (change categories)*/
- background: #DAD4BB;
- padding: 2px 6px;
-}
-
-QMenu QCheckBox:hover {
- /* checkboxes on right click menus when moused-over (change categories) */
- background: #B4AF9A;
-}
-
-QMenu QRadioButton {
- /* radio buttons on right click menus (primary categories) */
- background: #DAD4BB;
- padding: 2px 6px;
-}
-
-QToolTip {
- /* all tooltips */
- background: #DAD4BB;
- border: 2px solid #CDC8B0;
-}
-
-QStatusBar::item {border: None;}
-
-/* Progress Bars (Downloads) */
-
-QProgressBar {
- /* progress bars when downloading */
- background: transparent;
- text-align: center;
- border: 0px;
- margin: 0px 10px;
-}
-
-QProgressBar::chunk {
- /* the loading part that moves on progress bars */
- background: #CD664D;
-}
-
-/* Right Pane and Tab Bars */
-
-QTabWidget::pane {
- /* right pane */
- top: 1px;
- padding: 2px 2px 3px 2px;
- border-top: 2px solid #4E4B42;
-}
-
-QTabWidget::tab-bar {
- /* tabs */
- alignment: center;
-}
-
-QTabBar::tab {
- /* a tab */
- background: #B4AF9A;
- padding: 4px 1em;
- border: 2px solid #DAD4BB;
- margin: 3px 1px;
-}
-
-QTabBar::tab:!selected {
- /* an unselected tab */
- background: #B4AF9A;
- border: 2px solid #B4AF9A;
-}
-
-QTabBar::tab:disabled {
- /* An unavailable tab */
- background: transparent;
- color: #B4AF9A;
- border: transparent;
-}
-
-QTabBar::tab:selected {
- /* a clicked tab */
- color: #CDC8B0;
- background: #4E4B42;
- border: 2px solid #4E4B42;
-}
-
-QTabBar::tab:hover {
- /* a tab when hovered */
- background: #DAD4BB;
- color: #4E4B42;
- border: 2px solid #DAD4BB;
-}
-
-QTabBar QToolButton {
- /* buttons to scroll between more tabs on a tab bar */
- background: #CD664D;
- padding: 0px;
- margin: 3px;
-}
-
-QTabBar QToolButton:disabled {
- /* buttons to scroll on a tab bar when it's unavailable */
- background: transparent;
- border: 2px solid transparent;
-}
-
-QLCDNumber {
- /* LCD number on the Conflicts tab */
- background: #DAD4BB;
- color: #4E4B42;
- border: none;
-}
-
-/* Tables (Configure Mod Categories) */
-
-QTableView {
- /* tables */
- gridline-color: #CDC8B0;
- border: 0px;
-}
-
-/* Checkboxes */
-
-QCheckBox::indicator {
- /* a checkbox */
- width: 12px;
- height: 12px;
-}
-
-QCheckBox::indicator:disabled,
-QRadioButton::indicator:disabled {
- /* a checkbox that is disabled */
- background: none;
- border: 2px solid #B4AF9A;
-}
-
-QTreeView::indicator:unchecked,
-QCheckBox::indicator:unchecked,
-QGroupBox::indicator:unchecked,
-QRadioButton::indicator:unchecked {
- /* a checkbox when unchecked */
- border: 2px solid #4E4B42;
-}
-
-QCheckBox::indicator:unchecked:hover,
-QRadioButton::indicator:unchecked:hover {
- /* a checkbox that is unchecked when moused-over */
- background: #B4AF9A;
-}
-
-QTreeView::indicator:unchecked:selected,
-QRadioButton::indicator:unchecked:selected {
- /* a checkbox that is unchecked when clicked */
- border: 2px solid #DAD4BB;
-}
-
-QTreeView::indicator:checked,
-QCheckBox::indicator:checked,
-QGroupBox::indicator:checked,
-QRadioButton::indicator:checked {
- /* a checkbox when checked */
- background: #CD664D;
- border: 2px solid #CD664D;
-}
-
-QCheckBox::indicator:checked:hover,
-QRadioButton::indicator:checked:hover {
- /* a checkbox that is checked when moused-over */
- border: 2px solid #4E4B42;
-}
-
-/* Spinboxes */
-
-QSpinBox,
-QDoubleSpinBox {
- /* usually boxes for selecting numbers */
- min-height: 24px;
- min-width: 60px;
- background: #DAD4BB;
- padding: 0px 2px;
- border: 2px solid #CDC8B0;
- margin: 0px -4px;
-}
-
-QSpinBox::up-button,
-QDoubleSpinBox::up-button {
- /* up button on spinboxes */
- min-height: 28px;
- min-width: 18px;
- subcontrol-position: center right;
- border: 2px solid #CDC8B0;
-}
-
-QSpinBox::up-arrow,
-QDoubleSpinBox::up-arrow {
- /* arrow for the up button */
- image: url(./Paper/Automata/Arrows/up.svg);
-}
-
-QSpinBox::up-button:hover,
-QDoubleSpinBox::up-button:hover {
- /* up button on spinboxes when moused-over */
- background: #B4AF9A;
-}
-
-QSpinBox::down-button,
-QDoubleSpinBox::down-button {
- /* down button on spinboxes */
- min-height: 28px;
- min-width: 18px;
- subcontrol-position: center left;
- border: 2px solid #CDC8B0;
-}
-
-QSpinBox::down-arrow,
-QDoubleSpinBox::down-arrow {
- /* arrow for the up button */
- image: url(./Paper/Automata/Arrows/down.svg);
-}
-
-QSpinBox::down-button:hover,
-QDoubleSpinBox::down-button:hover {
- /* down button on spinboxes when moused-over */
- background: #B4AF9A;
-}
-
-/* Sliders */
-
-QSlider::groove {
- /* sliders */
- height: 0px;
- border: 1px solid #B4AF9A;
-}
-
-QSlider::handle {
- /* slider handles */
- background: #DAD4BB;
- border: 2px solid #4E4B42;
- border-radius: 0px;
- margin: -10px;
-}
-
-QSlider::handle:hover {
- /* Slider handles when moused-over */
- background: #4E4B42;
-}
-
-/* Downloads Tab */
-
-#downloadTab QAbstractScrollArea,
-DownloadListWidget {
- /* background of the entire downloads tab */
- background: #DAD4BB;
-}
-
-DownloadListWidget QFrame,
-DownloadListWidgetCompact,
-DownloadListWidgetCompact QLabel {
- /* an entry on the Downloads tab */
- background: #DAD4BB;
-}
-
-DownloadListWidget#frame {
- /* outer box of an entry on the Downloads tab */
- border: 2px solid #DAD4BB;
-}
-
-#installLabel {
- /* installed/done label */
- color: none;
-}
-
-DownloadListWidgetCompact,
-DownloadListWidgetCompact QLabel {
- /* an entry on the Downloads tab */
- background: #DAD4BB;
-}
-
-/* New Downloads View */
-
-DownloadListWidget[downloadView=standard]::item {
- /* Entries on the Standard Downloads View */
- min-height: 44px;
- margin: -16px 0;
-}
-
-DownloadListWidget[downloadView=compact]::item {
- /* Entries on the Compact Downloads View */
- min-height: 22px;
- margin: -4px 0;
-}
-
-QProgressBar[downloadView=standard] {
- /* Progress Bars on the Standard Downloads View */
- background: transparent;
- margin: 11px 0;
-}
-
-QProgressBar[downloadView=compact] {
- /* Progress Bars on the Compact Downloads View */
- background: transparent;
-}
-
-/* Categories Filter */
-
-#displayCategoriesBtn {
- /* Filter button */
- min-width: 12px;
-}
-
-#categoriesList {
- /* Categories panel */
- min-width: 200px;
- margin-bottom: 4px;
-}
-
-#categoriesGroup {
- /* Categories group box */
- padding-bottom: 0;
- margin-top: 3px;
-}
-
-/* Fixes */
-
-#executablesListBox {
- /* Increase right margin of the select executables box */
- margin-right: 8px;
-}
-
-#stepsStack QWidget {
- /* Groupboxes on the FOMOD Installer Dialog */
- background: #CDC8B0;
- border: none;
-}
-
-#stepsStack QGroupBox {
- /* Fix to reimplement styling for Groupboxes on the FOMOD Installer dialog */
- border: 2px solid #4E4B42;
-}
-
-#activeModslabel, #activePluginsLabel {
- /* Increase the left margin of the counters */
- padding-left: 6px;
-}
-
-/* For the Glory of Mankind */
diff --git a/src/stylesheets/Paper Dark by 6788.qss b/src/stylesheets/Paper Dark by 6788.qss
deleted file mode 100644
index 77086c15..00000000
--- a/src/stylesheets/Paper Dark by 6788.qss
+++ /dev/null
@@ -1,1026 +0,0 @@
-/* v5.0 Paper Dark by 6788-00 */
-/* https://6788-00.tumblr.com/ */
-
-/* Color Palette */
-/* Background - Main | #242424 */
-/* Background - Content | #141414 */
-/* Background - Alternate | #1C1C1C */
-/* Hover | #3D3D3D */
-/* Selected | #006868 */
-
-/* All */
-
-* {
- color: #D3D3D3;
- font-size: 12px;
-}
-
-/* Main Window */
-
-QWidget {
- /* most of the window */
- background: #242424;
- color: #D3D3D3;
-}
-
-QWidget:disabled {
- /* disabled parts of the window like the update button when there are no updates */
- background: #242424;
- color: #808080;
-}
-
-QAbstractItemView {
- /* left and right pane container */
- background: #141414;
- alternate-background-color: #1C1C1C;
- show-decoration-selected: 1;
- selection-background-color: #006868;
- selection-color: #FFFFFF;
-}
-
-QAbstractItemView::item {
- min-height: 22px;
-}
-
-QAbstractItemView::item:hover {
- /* rows on left and right pane when moused-over */
- background: #3D3D3D;
- color: #FFFFFF;
-}
-
-QAbstractItemView::item:selected {
- /* rows on left and right pane when clicked */
- background: #006868;
- color: #FFFFFF;
-}
-
-QAbstractScrollArea::corner {
- /* corner between where a vertical scrollbar and a horizontal scrollbar meet */
- background: #141414;
- border: 2px solid #242424;
- border-bottom-right-radius: 6px;
- margin: 0px -2px -2px 0px;
-}
-
-QSplitter {
- width: 8px;
-}
-
-/* Toolbar */
-
-QToolBar {
- /* top toolbar; */
- background: #242424;
- border: 1px solid #242424;
-}
-
-QToolBar::separator {
- /* uncertain, assumed: vertical seperator on toolbar left of the warnings button*/
- background: #242424;
- margin: 6px 8px;
-}
-
-QToolButton {
- /* toolbar buttons */
- padding: 6px;
- border-radius: 10px;
- margin: 4px 4px 0px 4px;
-}
-
-QToolButton:hover {
- /* toolbar buttons when moused-over; */
- background: #006868;
-}
-
-QToolButton:pressed {
- /* toolbar buttons when clicked; */
- background: #006868;
-}
-
-QToolButton:menu-indicator {
- /* expandable indicator on toolbar buttons */
- image: url(./Paper/Dark/Arrows/down.svg);
- margin: 4px;
-}
-
-/* Toolbar Button Icons */
-
-#actionChange_Game {
- qproperty-icon: url(./Paper/Dark/Toolbar/instances.svg);
-}
-
-#actionInstallMod {
- qproperty-icon: url(./Paper/Dark/Toolbar/archives.svg);
-}
-
-#actionNexus {
- qproperty-icon: url(./Paper/Dark/Toolbar/nexus.svg);
-}
-
-#actionAdd_Profile {
- qproperty-icon: url(./Paper/Dark/Toolbar/profiles.svg);
-}
-
-#actionModify_Executables {
- qproperty-icon: url(./Paper/Dark/Toolbar/executables.svg);
-}
-
-#actionTool {
- qproperty-icon: url(./Paper/Dark/Toolbar/tools.svg);
-}
-
-#actionSettings {
- qproperty-icon: url(./Paper/Dark/Toolbar/settings.svg);
-}
-
-#actionNotifications {
- qproperty-icon: url(./Paper/Dark/Toolbar/problems.svg);
-}
-
-#actionUpdate {
- qproperty-icon: url(./Paper/Dark/Toolbar/update.svg);
-}
-
-#actionHelp {
- qproperty-icon: url(./Paper/Dark/Toolbar/help.svg);
-}
-
-/* Left Pane & File Trees */
-
-ModListView, PluginListView {
- /* Mods List and Plugins List specifically */
- margin: 2px 0px;
-}
-
-QTreeView {
- /* left pane and right pane under QAbstractItemView*/
- border-radius: 6px;
-}
-
-QTreeView::branch:hover {
- /* rows on the left pane when moused-over (below QAbstractItemView, i.e. to the left of the checkbox) */
- background: #3A3A3A;
- color: #FFFFFF;
-}
-
-QTreeView::branch:selected {
- /* rows on the left pane when clicked (below QAbstractItemView, i.e. to the left of the checkbox) */
- background: #006868;
- color: #FFFFFF;
-}
-
-QTreeView::branch:closed:has-children:has-siblings,
-QTreeView::branch:closed:has-children:!has-siblings {
- /* a branch that is closed */
- image: url(./Paper/Dark/Arrows/right.svg);
-}
-
-QTreeView::branch:open:has-children:has-siblings,
-QTreeView::branch:open:has-children:!has-siblings {
- /* a branch that is open */
- image: url(./Paper/Dark/Arrows/down.svg);
-}
-
-QListView {
- /* saves window */
- border-radius: 6px;
-}
-
-QListView::item:hover {
- /* a row on the saves tab when moused-over */
- background: #3D3D3D;
- color: #FFFFFF;
- padding: 0;
-}
-
-QListView::item:selected {
- /* a row on the saves tab when clicked */
- background: #006868;
- color: #FFFFFF;
- padding: 0;
-}
-
-QTextEdit {
- /* large text fields */
- background: #141414;
- border-radius: 6px;
-}
-
-/* Group Boxes */
-
-QGroupBox {
- /* boxes that group multiple elements together (e.g. on Settings) */
- padding: 24px 4px;
- border: 2px solid #141414;
- border-radius: 10px;
-}
-
-QGroupBox::title {
- /* title of group boxes */
- background: transparent;
- subcontrol-origin: padding;
- subcontrol-position: top left;
- padding: 8px;
-}
-
-/* Text Fields */
-
-QLineEdit {
- /* text fields like NameFilter and directory fields */
- background: #141414;
- min-height: 14px;
- padding: 2px;
- border: 2px solid #141414;
- border-radius: 6px;
-}
-
-QLineEdit:hover {
- /* text fields when moused-over */
- border: 2px solid #006868;
-}
-
-/* Most Dropdown Menus */
-
-QComboBox {
- /* dropdown menus */
- background: #141414;
- min-height: 20px;
- padding-left: 5px;
- border: 2px solid #141414;
- border-radius: 6px;
- margin: 4px 0px;
-}
-
-QComboBox:hover {
- /* dropdown menus when moused-over */
- border: 2px solid #006868;
-}
-
-QComboBox:on {
- /* dropdown menus when expanded */
- background: #006868;
- color: #FFFFFF;
- border: 2px solid #006868;
-}
-
-QComboBox::drop-down {
- /* area for expandable indicator */
- width: 20px;
- subcontrol-origin: padding;
- subcontrol-position: top right;
- border: none;
-}
-
-QComboBox QAbstractItemView {
- /* actual menu that expands */
- background: #141414;
- border: 2px solid #242424;
- border-radius: 6px;
-}
-
-QComboBox::down-arrow {
- /* expandable indicator */
- image: url(./Paper/Dark/Arrows/down.svg);
-}
-
-/* Most Buttons */
-
-QPushButton {
- /* most buttons */
- background: #141414;
- color: #D3D3D3;
- min-height: 20px;
- padding: 2px 12px;
- border-radius: 6px;
- margin: 2px 0px;
-}
-
-QPushButton:disabled {
- /* most buttons when disabled */
- border: 2px solid #141414;
-}
-
-QPushButton:hover {
- /* most buttons when hovered */
- background: #006868;
- color: #FFFFFF;
-}
-
-QPushButton:pressed {
- /* most buttons when clicked */
- background: #006868;
- color: #FFFFFF;
-}
-
-QPushButton::menu-indicator {
- /* expandable indicator for most buttons */
- subcontrol-position: right center;
- image: url(./Paper/Dark/Arrows/down.svg);
- padding: 2px;
- margin: 4px 4px;
-}
-
-/* Icons */
-
-#listOptionsBtn {
- /* Options button */
- qproperty-icon: url(./Paper/Dark/dots.svg);
- qproperty-iconSize: 16px;
- padding-left: 2px;
-}
-
-#openFolderMenu {
- /* Open Folder button */
- qproperty-icon: url(./Paper/Dark/folder.svg);
- qproperty-iconSize: 14px;
- padding-left: 4px;
-}
-
-#restoreModsButton,
-#restoreButton {
- /* Restore Backup buttons */
- qproperty-icon: url(./Paper/Dark/restore.svg);
- qproperty-iconSize: 14px;
-}
-
-#saveModsButton,
-#saveButton {
- /* Backup buttons */
- qproperty-icon: url(./Paper/Dark/backup.svg);
- qproperty-iconSize: 14px;
-}
-
-#bossButton {
- /* Sort button */
- qproperty-icon: url(./Paper/Dark/sort.svg);
- qproperty-iconSize: 14px;
-}
-
-#linkButton {
- /* Shortcuts button */
- qproperty-icon: url(./Paper/Dark/shortcut.svg);
- qproperty-iconSize: 14px;
-}
-
-#refreshButton,
-#btnRefreshData,
-#btnRefreshDownloads {
- /* Refresh buttons */
- qproperty-icon: url(./Paper/Dark/refresh.svg);
- qproperty-iconSize: 14px;
-}
-
-#endorseBtn {
- /* Endorse button on the Nexus Info tab of the Information window */
- qproperty-icon: url(./Paper/Dark/heart.svg);
- qproperty-iconSize: 14px;
-}
-
-#clearCacheButton {
- /* Clear Cache button on the Nexus tab of the Settings window */
- qproperty-icon: url(./Paper/Dark/cross.svg);
- qproperty-iconSize: 14px;
-}
-
-#deactivateESP,
-#activateESP {
- /* activate and deactivate ESP buttons */
- background: #141414;
-}
-
-#deactivateESP:hover,
-#activateESP:hover {
- /* activate and deactivate ESP buttons when moused-over */
- background: #006868;
-}
-
-#deactivateESP {
- /* icon for the deactivate ESP button */
- qproperty-icon: url(./Paper/Dark/backup.svg);
-}
-
-#activateESP {
- /* icon for the activate ESP button */
- qproperty-icon: url(./Paper/Dark/restore-alt.svg);
-}
-
-/* Run button */
-
-#startButton {
- /* Run button */
- background: #006868;
- color: #FFFFFF;
- qproperty-icon: url(./Paper/Dark/run.svg);
- qproperty-iconSize: 30px;
- padding: 6px;
-}
-
-#startButton:hover {
- /* Run button when moused-over*/
- background: #3D3D3D;
-}
-
-/* Scroll Bars */
-
-/* Horizontal */
-
-QScrollBar:horizontal {
- /* horizontal scroll bar */
- background: #141414;
- height: 20px;
- border: 2px solid #242424;
- margin: 0px 23px -2px 23px;
-}
-
-QScrollBar::handle:horizontal {
- /* handle for horizontal scroll bars */
- background: #3D3D3D;
- min-width: 32px;
- border-radius: 6px;
- margin: 2px;
-}
-
-QScrollBar::add-line:horizontal {
- /* scroll right button */
- background: #141414;
- image: url(./Paper/Dark/Arrows/right.svg);
- width: 23px;
- subcontrol-position: right;
- subcontrol-origin: margin;
- border: 2px solid #242424;
- margin: 0px -2px -2px 0px;
-}
-
-QScrollBar::sub-line:horizontal {
- /* scroll left button */
- background: #141414;
- image: url(./Paper/Dark/Arrows/left.svg);
- width: 23px;
- subcontrol-position: left;
- subcontrol-origin: margin;
- border: 2px solid #242424;
- border-bottom-left-radius: 6px;
- margin: 0px 0px -2px -2px;
-}
-
-/* Vertical */
-
-QScrollBar:vertical {
- /* vertical scroll bar */
- background: #141414;
- width: 20px;
- border: 2px solid #242424;
- margin: 23px -2px 23px 0px;
-}
-
-QScrollBar::handle:vertical {
- /* handle for vertical scroll bars */
- background: #3D3D3D;
- min-height: 32px;
- border-radius: 6px;
- margin: 2px;
-}
-
-QScrollBar::add-line:vertical {
- /* scroll down button */
- background: #141414;
- image: url(./Paper/Dark/Arrows/down.svg);
- height: 23px;
- subcontrol-position: bottom;
- subcontrol-origin: margin;
- border: 2px solid #242424;
- border-bottom-right-radius: 6px;
- margin: 0px -2px -2px 0px;
-}
-
-QScrollBar::sub-line:vertical {
- /* scroll up button */
- background: #141414;
- image: url(./Paper/Dark/Arrows/up.svg);
- height: 23px;
- subcontrol-position: top;
- subcontrol-origin: margin;
- border: 2px solid #242424;
- border-top-right-radius: 6px;
- margin: -2px -2px 0px 0px;
-}
-
-/* Combined */
-
-QScrollBar::handle:horizontal:hover,
-QScrollBar::handle:vertical:hover,
-QScrollBar::add-line:horizontal:hover,
-QScrollBar::sub-line:horizontal:hover,
-QScrollBar::add-line:vertical:hover,
-QScrollBar::sub-line:vertical:hover {
- /* buttons and handles when moused-over */
- background: #006868;
-}
-
-QScrollBar::handle:horizontal:pressed,
-QScrollBar::handle:vertical:pressed,
-QScrollBar::add-line:horizontal:pressed,
-QScrollBar::sub-line:horizontal:pressed,
-QScrollBar::add-line:vertical:pressed,
-QScrollBar::sub-line:vertical:pressed {
- /* buttons and handles when clicked */
- background: #006868;
-}
-
-QScrollBar::add-page:horizontal,
-QScrollBar::sub-page:horizontal,
-QScrollBar::add-page:vertical,
-QScrollBar::sub-page:vertical {
- /* area on scroll bars where clicking it scrolls to where you clicked */
- background: transparent;
-}
-
-/* Header Rows */
-
-QHeaderView {
- /* header row (i.e. Mod Name, Flags, Category, etc.) */
- background: #242424;
-}
-
-QHeaderView::section {
- /* each section on the header row (i.e. Mod name is one section and Flags another) */
- background: #141414;
- color: #D3D3D3;
- height: 22px;
- padding: 0px 5px;
- border: 0px;
- border-bottom: 2px solid #242424;
- border-right: 2px solid #242424;
-}
-
-QHeaderView::section:first {
- /* first section on a header row */
- border-top-left-radius: 6px;
-}
-
-QHeaderView::section:last {
- /* last section on a header row */
- border-right: 0px;
- border-top-right-radius: 6px;
-}
-
-QHeaderView::section:hover {
- /* a section on a header row when hovered */
- background: #3D3D3D;
- color: #FFFFFF;
-}
-
-QHeaderView::up-arrow {
- /* ascending sort indicator */
- image: url(./Paper/Dark/Arrows/up.svg);
- padding-right: 4px;
- height: 10px;
- width: 10px;
-}
-
-QHeaderView::down-arrow {
- /* descending sort indicator */
- image: url(./Paper/Dark/Arrows/down.svg);
- padding-right: 4px;
- height: 10px;
- width: 10px;
-}
-
-/* Right Click Menus, Toolbar Dropdown Menus, & Tooltips */
-
-QMenuBar {
- background: #242424;
- border: 1px solid #242424;
-}
-
-QMenuBar::item:selected {
- background: #006868;
- color: #FFFFFF;
- border: 0px;
- border-radius: 4px;
-}
-
-QMenu {
- /* right click menu */
- background: #141414;
- border: 2px solid #242424;
- border-radius: 6px;
-}
-
-QMenu::item {
- /* rows on right click menus */
- background: #141414;
- padding: 5px 20px 5px 24px;
-}
-
-QMenu::item:selected {
- /* rows on right click menus when moused-over (i dunno) */
- background: #006868;
- color: #FFFFFF;
- border: 0px;
- border-radius: 4px;
-}
-
-QMenu::item:disabled {
- /* unavailable rows on right click menus */
- background: #242424;
- color: #808080;
-}
-
-QMenu::separator {
- /* seperators on right click menus */
- height: 2px;
- background: #242424;
-}
-
-QMenu::icon {
- /* area for icons on right click menus */
- padding: 4px;
-}
-
-QMenu::right-arrow {
- /* submenu indicator */
- image: url(./Paper/Dark/Arrows/right.svg);
- padding-right: 5px;
-}
-
-QMenu QPushButton {
- /* Change Categories and Primary Categories buttons */
- background: #141414;
- color: #D3D3D3;
- padding: 2px 24px;
- text-align: left;
- border-radius: 0px;
-}
-
-QMenu QPushButton:hover {
- /* Change Categories and Primary Categories buttons when moused-over */
- border-radius: 6px;
-}
-
-QMenu QCheckBox {
- /* checkboxes on right click menus (change categories)*/
- background: #141414;
- padding: 2px 6px;
-}
-
-QMenu QCheckBox:hover {
- /* checkboxes on right click menus when moused-over (change categories) */
- background: #3D3D3D;
- color: #FFFFFF;
-}
-
-QMenu QRadioButton {
- /* radio buttons on right click menus (primary categories) */
- background: #141414;
- padding: 2px 6px;
-}
-
-QToolTip {
- /* all tooltips */
- background: #141414;
- border: 2px solid #242424;
- border-radius: 6px;
-}
-
-QStatusBar::item {border: None;}
-
-/* Progress Bars (Downloads) */
-
-QProgressBar {
- /* progress bars when downloading */
- background: #141414;
- color: #FFFFFF;
- text-align: center;
- border: 2px solid #242424;
- border-radius: 6px;
- margin: 0px 10px;
-}
-
-QProgressBar::chunk {
- /* the loading part that moves on progress bars */
- background: #006868;
- border-top-left-radius: 6px;
- border-bottom-left-radius: 6px;
-}
-
-/* Right Pane and Tab Bars */
-
-QTabWidget::pane {
- /* right pane */
- top: 1px;
- padding: 2px 2px 10px 2px;
- border: 2px solid #141414;
- border-radius: 10px;
-}
-
-QTabWidget::tab-bar {
- /* tabs */
- alignment: center;
-}
-
-QTabBar::tab {
- /* a tab */
- background: #141414;
- color: #141414;
- padding: 4px 1em;
- border: 2px solid #141414;
- margin: 3px 1px;
-}
-
-QTabBar::tab:!selected {
- /* a tab that is not clicked */
- background: #141414;
- color: #D3D3D3;
- border: 2px solid #141414;
-}
-
-QTabBar::tab:disabled {
- /* a tab that is disabled */
- background: #242424;
- color: #808080;
- border: 2px solid transparent;
-}
-
-QTabBar::tab:selected {
- /* a tab that is clicked */
- color: #FFFFFF;
- background: #006868;
- border: 2px solid #006868;
-}
-
-QTabBar::tab:hover {
- /* a tab when moused-over */
- color: #FFFFFF;
- background: #3D3D3D;
- border: 2px solid #3D3D3D;
-}
-
-QTabBar::tab:first {
- /* that first tab */
- border-top-left-radius: 10px;
- border-bottom-left-radius: 10px;
-}
-
-QTabBar::tab:last {
- /* the last tab */
- border-top-right-radius: 10px;
- border-bottom-right-radius: 10px;
-}
-
-QTabBar QToolButton {
- /* buttons to scroll between more tabs on a tab bar */
- background: #3D3D3D;
- padding: 1px;
- border-radius: 6px;
- margin: 1px;
-}
-
-QTabBar QToolButton:disabled {
- /* buttons to scroll on a tab bar when it's unavailable */
- background: transparent;
-}
-
-QLCDNumber {
- /* LCD number on the Conflicts tab */
- background: #141414;
- color: #006868;
- border-radius: 6px;
-
-}
-
-/* Tables (Configure Mod Categories) */
-
-QTableView {
- /* a table */
- gridline-color: #242424;
- border: 0px;
-}
-
-/* Checkboxes */
-
-QTreeView::indicator:unchecked,
-QCheckBox::indicator:unchecked,
-QGroupBox::indicator:unchecked,
-QRadioButton::indicator:unchecked {
- /* a checkbox that is unchecked */
- image: url(./Paper/Dark/unchecked.svg);
- width: 14px;
- height: 14px;
-}
-
-QCheckBox::indicator:unchecked:hover,
-QRadioButton::indicator:unchecked:hover {
- /* a checkbox that is unchecked when moused-over and clicked */
- image: url(./Paper/Dark/unchecked-alt.svg);
-}
-
-QTreeView::indicator:checked,
-QCheckBox::indicator:checked,
-QGroupBox::indicator:checked,
-QRadioButton::indicator:checked {
- /* a checkbox that is checked */
- image: url(./Paper/Dark/check.svg);
- width: 14px;
- height: 14px;
-}
-
-QTreeView::indicator:checked:selected,
-QCheckBox::indicator:checked:hover,
-QRadioButton::indicator:checked:hover {
- /* a checkbox that is checked when moused-over and clicked */
- image: url(./Paper/Dark/check-alt.svg);
-}
-
-QCheckBox::indicator:disabled {
- /* a checkbox that is disabled */
- image: url(./Paper/Dark/unchecked-disabled.svg);
-}
-
-/* Spinboxes */
-
-QSpinBox,
-QDoubleSpinBox {
- /* usually boxes for selecting numbers */
- min-height: 24px;
- min-width: 60px;
- background: #141414;
- padding: 0px 2px;
- border: 2px solid #242424;
- border-radius: 6px;
- margin: 0px -4px;
-}
-
-QSpinBox::up-button,
-QDoubleSpinBox::up-button {
- /* up button on spinboxes */
- min-height: 28px;
- min-width: 20px;
- subcontrol-position: center right;
- border: 2px solid #242424;
- border-top-right-radius: 6px;
- border-bottom-right-radius: 6px;
-}
-
-QSpinBox::up-arrow,
-QDoubleSpinBox::up-arrow {
- /* arrow for the up button */
- image: url(./Paper/Dark/Arrows/up.svg);
-}
-
-QSpinBox::up-button:hover,
-QDoubleSpinBox::up-button:hover {
- /* up button on spinboxes when moused-over */
- background: #3D3D3D;
-}
-
-QSpinBox::down-button,
-QDoubleSpinBox::down-button {
- /* down button on spinboxes */
- min-height: 28px;
- min-width: 20px;
- subcontrol-position: center left;
- border: 2px solid #242424;
- border-top-left-radius: 6px;
- border-bottom-left-radius: 6px;
-}
-
-QSpinBox::down-arrow,
-QDoubleSpinBox::down-arrow {
- /* arrow for the up button */
- image: url(./Paper/Dark/Arrows/down.svg);
-}
-
-QSpinBox::down-button:hover,
-QDoubleSpinBox::down-button:hover {
- /* down button on spinboxes when moused-over */
- background: #3D3D3D;
-}
-
-/* Sliders */
-
-QSlider::groove {
- /* sliders */
- height: 0px;
- border: 1px solid #3D3D3D;
-}
-
-QSlider::handle {
- /* slider handles */
- background: #141414;
- border: 2px solid #3D3D3D;
- border-radius: 6px;
- margin: -10px;
-}
-
-QSlider::handle:hover {
- /* Slider handles when moused-over */
- background: #3D3D3D;
-}
-
-/* Pre-v2.1.7 Downloads Tab */
-
-#downloadTab QAbstractScrollArea {
- /* background of the entire downloads tab */
- background: #141414;
-}
-
-DownloadListWidget QFrame,
-DownloadListWidgetCompact,
-DownloadListWidgetCompact QLabel {
- /* an entry on the Downloads tab */
- background: #141414;
-}
-
-DownloadListWidget#frame {
- /* outer box of an entry on the Downloads tab */
- border: none;
-}
-
-#installLabel {
- /* installed/done label */
- color: none;
-}
-
-DownloadListWidgetCompact,
-DownloadListWidgetCompact QLabel {
- /* an entry on the Downloads tab in Compact View */
- background: #141414;
-}
-
-/* New Downloads View */
-
-DownloadListWidget[downloadView=standard]::item {
- /* Entries on the Standard Downloads View */
- min-height: 44px;
- margin: -16px 0;
-}
-
-DownloadListWidget[downloadView=compact]::item {
- /* Entries on the Compact Downloads View */
- min-height: 22px;
- margin: -4px 0;
-}
-
-QProgressBar[downloadView=standard] {
- /* Progress Bars on the Standard Downloads View */
- background: transparent;
- margin: 11px 0;
-}
-
-QProgressBar[downloadView=standard]::chunk,
-QProgressBar[downloadView=compact]::chunk {
- /* The Loading Portion of Progress Bars on the Downloads View */
- border-right: 2px solid #242424;
-}
-
-QProgressBar[downloadView=compact] {
- /* Progress Bars on the Compact Downloads View */
- background: transparent;
-}
-
-/* Categories Filter */
-
-#displayCategoriesBtn {
- /* Filter button */
- min-width: 12px;
-}
-
-#categoriesList {
- /* Categories panel */
- min-width: 200px;
- margin-bottom: 4px;
-}
-
-#categoriesGroup {
- /* Categories group box */
- padding-bottom: 0px;
-}
-
-/* Fixes */
-
-#executablesListBox {
- /* Increase right margin of the select executables box */
- margin-right: 8px;
-}
-
-#executablesListBox::item {
- /* fixes the black text problem on the Modify Executables window */
- color: #D3D3D3;
-}
-
-#stepsStack QWidget {
- /* Groupboxes on the FOMOD Installer Dialog */
- border: none;
-}
-
-#stepsStack QGroupBox {
- /* Fix to reimplement styling for Groupboxes on the FOMOD Installer dialog */
- border: 2px solid #141414;
- border-radius: 10px;
-}
-
-#activeModslabel, #activePluginsLabel {
- /* Increase the left margin of the counters */
- padding-left: 6px;
-}
diff --git a/src/stylesheets/Paper Light by 6788.qss b/src/stylesheets/Paper Light by 6788.qss
deleted file mode 100644
index 2b08bcd1..00000000
--- a/src/stylesheets/Paper Light by 6788.qss
+++ /dev/null
@@ -1,1009 +0,0 @@
-/* v5.0 Paper Light by 6788-00 */
-/* https://6788-00.tumblr.com/ */
-
-/* Color Palette */
-/* Background - Main | #EBEBEB */
-/* Background - Content | #FFFFFF */
-/* Background - Alternate | #F6F6F6 */
-/* Hover | #C2C2C2 */
-/* Selected | #008484 */
-
-/* All */
-
-* {
- color: #000000;
- font-size: 12px;
-}
-
-/* Main Window */
-
-QWidget {
- /* most of the window */
- background: #EBEBEB;
- color: #000000;
-}
-
-QWidget:disabled {
- /* disabled parts of the window like the update button when there are no updates */
- background: #EBEBEB;
- color: #808080;
-}
-
-QAbstractItemView {
- /* left and right pane container */
- background: #FFFFFF;
- alternate-background-color: #F6F6F6;
- show-decoration-selected: 1;
- selection-background-color: #008484;
- selection-color: #FFFFFF;
-}
-
-QAbstractItemView::item {
- min-height: 22px;
-}
-
-QAbstractItemView::item:hover {
- /* rows on left and right pane when moused-over */
- background: #C2C2C2;
-}
-
-QAbstractItemView::item:selected {
- /* rows on left and right pane when clicked */
- background: #008484;
- color: #FFFFFF;
-}
-
-QAbstractScrollArea::corner {
- /* corner between where a vertical scrollbar and a horizontal scrollbar meet */
- background: #FFFFFF;
- border: 2px solid #EBEBEB;
- border-bottom-right-radius: 6px;
- margin: 0px -2px -2px 0px;
-}
-
-QSplitter {
- width: 8px;
-}
-
-/* Toolbar */
-
-QToolBar {
- /* top toolbar; */
- background: #EBEBEB;
- border: 1px solid #EBEBEB;
-}
-
-QToolBar::separator {
- /* uncertain, assumed: vertical seperator on toolbar left of the warnings button*/
- background: #EBEBEB;
- margin: 6px 8px;
-}
-
-QToolButton {
- /* toolbar buttons */
- padding: 6px;
- border-radius: 10px;
- margin: 4px 4px 0px 4px;
-}
-
-QToolButton:hover {
- /* toolbar buttons when moused-over; */
- background: #008484;
-}
-
-QToolButton:pressed {
- /* toolbar buttons when clicked; */
- background: #008484;
-}
-
-QToolButton:menu-indicator {
- /* expandable indicator on toolbar buttons */
- image: url(./Paper/Light/Arrows/down.svg);
- margin: 4px;
-}
-
-/* Toolbar Button Icons */
-
-#actionChange_Game {
- qproperty-icon: url(./Paper/Light/Toolbar/instances.svg);
-}
-
-#actionInstallMod {
- qproperty-icon: url(./Paper/Light/Toolbar/archives.svg);
-}
-
-#actionNexus {
- qproperty-icon: url(./Paper/Light/Toolbar/nexus.svg);
-}
-
-#actionAdd_Profile {
- qproperty-icon: url(./Paper/Light/Toolbar/profiles.svg);
-}
-
-#actionModify_Executables {
- qproperty-icon: url(./Paper/Light/Toolbar/executables.svg);
-}
-
-#actionTool {
- qproperty-icon: url(./Paper/Light/Toolbar/tools.svg);
-}
-
-#actionSettings {
- qproperty-icon: url(./Paper/Light/Toolbar/settings.svg);
-}
-
-#actionNotifications {
- qproperty-icon: url(./Paper/Light/Toolbar/problems.svg);
-}
-
-#actionUpdate {
- qproperty-icon: url(./Paper/Light/Toolbar/update.svg);
-}
-
-#actionHelp {
- qproperty-icon: url(./Paper/Light/Toolbar/help.svg);
-}
-
-/* Left Pane & File Trees */
-
-ModListView, PluginListView {
- /* Mods List and Plugins List specifically */
- margin: 4px 0px;
-}
-
-QTreeView {
- /* left pane and right pane under QAbstractItemView*/
- border-radius: 6px;
-}
-
-QTreeView::branch:hover {
- /* rows on the left pane when moused-over (below QAbstractItemView, i.e. to the left of the checkbox) */
- background: #C2C2C2;
- color: #FFFFFF;
-}
-
-QTreeView::branch:selected {
- /* rows on the left pane when clicked (below QAbstractItemView, i.e. to the left of the checkbox) */
- background: #008484;
- color: #FFFFFF;
-}
-
-QTreeView::item:selected {
- /* rows on the left pane when selected */
- background: #008484;
- color: #FFFFFF;
-}
-
-QTreeView::branch:closed:has-children:has-siblings,
-QTreeView::branch:closed:has-children:!has-siblings {
- /* a branch that is closed */
- image: url(./Paper/Light/Arrows/right.svg);
-}
-
-QTreeView::branch:open:has-children:has-siblings,
-QTreeView::branch:open:has-children:!has-siblings {
- /* a branch that is open */
- image: url(./Paper/Light/Arrows/down.svg);
-}
-
-QListView {
- /* saves window */
- border-radius: 6px;
-}
-
-QListView::item:hover {
- /* uncertain, assumed: rows on the saves window when moused-over */
- background: #C2C2C2;
- padding: 0;
-}
-
-QListView::item:selected {
- /*uncertain, assumed: rows on the saves window when clicked */
- background: #008484;
- color: #FFFFFF;
- padding: 0;
-}
-
-QTextEdit {
- /* large text fields */
- background: #FFFFFF;
- border-radius: 6px;
-}
-
-/* Group Boxes */
-
-QGroupBox {
- /* boxes that group multiple elements together (e.g. on Settings) */
- padding: 24px 4px;
- border: 2px solid #FFFFFF;
- border-radius: 10px;
-}
-
-QGroupBox::title {
- /* title of group boxes */
- background: transparent;
- subcontrol-origin: padding;
- subcontrol-position: top left;
- padding: 8px;
-}
-
-/* Text Fields */
-
-QLineEdit {
- /* text fields like NameFilter and directory fields */
- background: #FFFFFF;
- min-height: 14px;
- padding: 2px;
- border: 2px solid #FFFFFF;
- border-radius: 6px;
-}
-
-QLineEdit:hover {
- /* text fields when moused-over */
- border: 2px solid #008484;
-}
-
-/* Most Dropdown Menus */
-
-QComboBox {
- /* dropdown menus */
- background: #FFFFFF;
- min-height: 20px;
- padding-left: 5px;
- border: 2px solid #FFFFFF;
- border-radius: 6px;
- margin: 4px 0px;
-}
-
-QComboBox:hover {
- /* dropdown menus when moused-over */
- border: 2px solid #008484;
-}
-
-QComboBox:on {
- /* dropdown menus when expanded */
- background: #008484;
- color: #FFFFFF;
- border: 2px solid #008484;
-}
-
-QComboBox::drop-down {
- /* area for expandable indicator */
- width: 20px;
- subcontrol-origin: padding;
- subcontrol-position: top right;
- border: none;
-}
-
-QComboBox QAbstractItemView {
- /* actual menu that expands */
- background: #FFFFFF;
- border: 2px solid #EBEBEB;
- border-radius: 6px;
-}
-
-QComboBox::down-arrow {
- /* expandable indicator */
- image: url(./Paper/Light/Arrows/down.svg);
-}
-
-/* Most Buttons */
-
-QPushButton {
- /* most buttons */
- background: #FFFFFF;
- color: #000000;
- min-height: 20px;
- padding: 2px 12px;
- border-radius: 6px;
-}
-
-QPushButton:disabled {
- /* most buttons when disabled */
- border: 2px solid #FFFFFF;
-}
-
-QPushButton:hover {
- /* most buttons when hovered */
- background: #008484;
- color: #FFFFFF;
-}
-
-QPushButton:pressed {
- /* most buttons when clicked */
- background: #008484;
- color: #FFFFFF;
-}
-
-QPushButton::menu-indicator {
- /* expandable indicator for most buttons */
- subcontrol-position: right center;
- image: url(./Paper/Light/Arrows/down.svg);
- padding: 2px;
- margin: 4px 4px;
-}
-
-/* Icons */
-
-#listOptionsBtn {
- /* Options button */
- qproperty-icon: url(./Paper/Light/dots.svg);
- qproperty-iconSize: 16px;
- padding-left: 2px;
-}
-
-#openFolderMenu {
- /* Open Folder button */
- qproperty-icon: url(./Paper/Light/folder.svg);
- qproperty-iconSize: 14px;
- padding-left: 4px;
-}
-
-#restoreModsButton,
-#restoreButton {
- /* Restore Backup buttons */
- qproperty-icon: url(./Paper/Light/restore.svg);
- qproperty-iconSize: 14px;
-}
-
-#saveModsButton,
-#saveButton {
- /* Backup buttons */
- qproperty-icon: url(./Paper/Light/backup.svg);
- qproperty-iconSize: 14px;
-}
-
-#bossButton {
- /* Sort button */
- qproperty-icon: url(./Paper/Light/sort.svg);
- qproperty-iconSize: 14px;
-}
-
-#linkButton {
- /* Shortcuts button */
- qproperty-icon: url(./Paper/Light/shortcut.svg);
- qproperty-iconSize: 14px;
-}
-
-#refreshButton,
-#btnRefreshData,
-#btnRefreshDownloads {
- /* Refresh buttons */
- qproperty-icon: url(./Paper/Light/refresh.svg);
- qproperty-iconSize: 14px;
-}
-
-#endorseBtn {
- /* Endorse button on the Nexus Info tab of the Information window */
- qproperty-icon: url(./Paper/Light/heart.svg);
- qproperty-iconSize: 14px;
-}
-
-#clearCacheButton {
- /* Clear Cache button on the Nexus tab of the Settings window */
- qproperty-icon: url(./Paper/Light/cross.svg);
- qproperty-iconSize: 14px;
-}
-
-#deactivateESP,
-#activateESP {
- /* activate and deactivate ESP buttons */
- background: #FFFFFF;
-}
-
-#deactivateESP:hover,
-#activateESP:hover {
- /* activate and deactivate ESP buttons when moused-over */
- background: #008484;
-}
-
-#deactivateESP {
- /* icon for the deactivate ESP button */
- qproperty-icon: url(./Paper/Light/backup.svg);
-}
-
-#activateESP {
- /* icon for the activate ESP button */
- qproperty-icon: url(./Paper/Light/restore-alt.svg);
-}
-
-/* Run button */
-
-#startButton {
- /* Run button */
- background: #008484;
- color: #FFFFFF;
- qproperty-icon: url(./Paper/Light/run.svg);
- qproperty-iconSize: 30px;
- padding: 6px;
-}
-
-#startButton:hover {
- /* Run button when moused-over*/
- background: #C2C2C2;
-}
-
-/* Scroll Bars */
-
-/* Horizontal */
-
-QScrollBar:horizontal {
- /* horizontal scroll bar */
- background: #FFFFFF;
- height: 20px;
- border: 2px solid #EBEBEB;
- margin: 0px 23px -2px 23px;
-}
-
-QScrollBar::handle:horizontal {
- /* handle for horizontal scroll bars */
- background: #C2C2C2;
- min-width: 32px;
- border-radius: 6px;
- margin: 2px;
-}
-
-QScrollBar::add-line:horizontal {
- /* scroll right button */
- background: #FFFFFF;
- image: url(./Paper/Light/Arrows/right.svg);
- width: 23px;
- subcontrol-position: right;
- subcontrol-origin: margin;
- border: 2px solid #EBEBEB;
- margin: 0px -2px -2px 0px;
-}
-
-QScrollBar::sub-line:horizontal {
- /* scroll left button */
- background: #FFFFFF;
- image: url(./Paper/Light/Arrows/left.svg);
- width: 23px;
- subcontrol-position: left;
- subcontrol-origin: margin;
- border: 2px solid #EBEBEB;
- border-bottom-left-radius: 6px;
- margin: 0px 0px -2px -2px;
-}
-
-/* Vertical */
-
-QScrollBar:vertical {
- /* vertical scroll bar */
- background: #FFFFFF;
- width: 20px;
- border: 2px solid #EBEBEB;
- margin: 23px -2px 23px 0px;
-}
-
-QScrollBar::handle:vertical {
- /* handle for vertical scroll bars */
- background: #C2C2C2;
- min-height: 32px;
- border-radius: 6px;
- margin: 2px;
-}
-
-QScrollBar::add-line:vertical {
- /* scroll down button */
- background: #FFFFFF;
- image: url(./Paper/Light/Arrows/down.svg);
- height: 23px;
- subcontrol-position: bottom;
- subcontrol-origin: margin;
- border: 2px solid #EBEBEB;
- border-bottom-right-radius: 6px;
- margin: 0px -2px -2px 0px;
-}
-
-QScrollBar::sub-line:vertical {
- /* scroll up button */
- background: #FFFFFF;
- image: url(./Paper/Light/Arrows/up.svg);
- height: 23px;
- subcontrol-position: top;
- subcontrol-origin: margin;
- border: 2px solid #EBEBEB;
- border-top-right-radius: 6px;
- margin: -2px -2px 0px 0px;
-}
-
-/* Combined */
-
-QScrollBar::handle:horizontal:hover,
-QScrollBar::handle:vertical:hover,
-QScrollBar::add-line:horizontal:hover,
-QScrollBar::sub-line:horizontal:hover,
-QScrollBar::add-line:vertical:hover,
-QScrollBar::sub-line:vertical:hover {
- /* buttons and handles when moused-over */
- background: #008484;
-}
-
-QScrollBar::handle:horizontal:pressed,
-QScrollBar::handle:vertical:pressed,
-QScrollBar::add-line:horizontal:pressed,
-QScrollBar::sub-line:horizontal:pressed,
-QScrollBar::add-line:vertical:pressed,
-QScrollBar::sub-line:vertical:pressed {
- /* buttons and handles when clicked */
- background: #008484;
-}
-
-QScrollBar::add-page:horizontal,
-QScrollBar::sub-page:horizontal,
-QScrollBar::add-page:vertical,
-QScrollBar::sub-page:vertical {
- /* area on scroll bars where clicking it scrolls to where you clicked */
- background: transparent;
-}
-
-/* Header Rows */
-
-QHeaderView {
- /* header row (i.e. Mod Name, Flags, Category, etc.) */
- background: #EBEBEB;
-}
-
-QHeaderView::section {
- /* each section on the header row (i.e. Mod name is one section and Flags another) */
- background: #FFFFFF;
- color: #000000;
- height: 22px;
- padding: 0px 5px;
- border: 0px;
- border-bottom: 2px solid #EBEBEB;
- border-right: 2px solid #EBEBEB;
-}
-
-QHeaderView::section:first {
- /* first section on a header row */
- border-top-left-radius: 6px;
-}
-
-QHeaderView::section:last {
- /* last section on a header row */
- border-right: 0px;
- border-top-right-radius: 6px;
-}
-
-QHeaderView::section:hover {
- /* a section on a header row when hovered */
- background: #C2C2C2;
-}
-
-QHeaderView::up-arrow {
- /* ascending sort indicator */
- image: url(./Paper/Light/Arrows/up.svg);
- padding-right: 4px;
- height: 10px;
- width: 10px;
-}
-
-QHeaderView::down-arrow {
- /* descending sort indicator */
- image: url(./Paper/Light/Arrows/down.svg);
- padding-right: 4px;
- height: 10px;
- width: 10px;
-}
-
-/* Right Click Menus, Toolbar Dropdown Menus, & Tooltips */
-
-QMenuBar {
- background: #EBEBEB;
- border: 1px solid #EBEBEB;
-}
-
-QMenuBar::item:selected {
- background: #008484;
- color: #FFFFFF;
- border: 0px;
- border-radius: 4px;
-}
-
-QMenu {
- /* right click menu */
- background: #FFFFFF;
- border: 2px solid #EBEBEB;
- border-radius: 6px;
-}
-
-QMenu::item {
- /* rows on right click menus */
- background: #FFFFFF;
- padding: 5px 20px 5px 24px;
-}
-
-QMenu::item:selected {
- /* rows on right click menus when moused-over (i dunno) */
- background: #008484;
- color: #FFFFFF;
- border: 0px;
- border-radius: 4px;
-}
-
-QMenu::item:disabled {
- /* unavailable rows on right click menus */
- background: #EBEBEB;
- color: #808080;
-}
-
-QMenu::separator {
- /* seperators on right click menus */
- height: 2px;
- background: #EBEBEB;
-}
-
-QMenu::icon {
- /* area for icons on right click menus */
- padding: 4px;
-}
-
-QMenu::right-arrow {
- /* submenu indicator */
- image: url(./Paper/Light/Arrows/right.svg);
- padding-right: 5px;
-}
-
-QMenu QPushButton {
- /* Change Categories and Primary Categories buttons */
- background: #FFFFFF;
- color: #000000;
- padding: 2px 24px;
- text-align: left;
- border-radius: 0px;
-}
-
-QMenu QPushButton:hover {
- /* Change Categories and Primary Categories buttons when moused-over */
- border-radius: 6px;
-}
-
-QMenu QCheckBox {
- /* checkboxes on right click menus (change categories)*/
- background: #FFFFFF;
- padding: 2px 6px;
-}
-
-QMenu QCheckBox:hover {
- /* checkboxes on right click menus when moused-over (change categories) */
- background: #C2C2C2;
- color: #FFFFFF;
-}
-
-QMenu QRadioButton {
- /* radio buttons on right click menus (primary categories) */
- background: #FFFFFF;
- padding: 2px 6px;
-}
-
-QToolTip {
- /* all tooltips */
- background: #FFFFFF;
- border: 2px solid #EBEBEB;
- border-radius: 6px;
-}
-
-QStatusBar::item {border: None;}
-
-/* Progress Bars (Downloads) */
-
-QProgressBar {
- /* progress bars when downloading */
- background: #FFFFFF;
- text-align: center;
- border: 0px;
- border-radius: 6px;
- margin: 0px 10px;
-}
-
-QProgressBar::chunk {
- /* the loading part that moves on progress bars */
- background: #C2C2C2;
- border-top-left-radius: 6px;
- border-bottom-left-radius: 6px;
-}
-
-/* Right Pane and Tab Bars */
-
-QTabWidget::pane {
- /* right pane */
- top: 1px;
- padding: 2px 2px 10px 2px;
- border: 2px solid #FFFFFF;
- border-radius: 10px;
-}
-
-QTabWidget::tab-bar {
- /* tabs */
- alignment: center;
-}
-
-QTabBar::tab {
- /* a tab */
- background: #FFFFFF;
- color: #000000;
- padding: 4px 1em;
- border: 2px solid #FFFFFF;
- margin: 3px 1px;
-}
-
-QTabBar::tab:!selected {
- /* a tab that is not clicked */
- background: #FFFFFF;
- color: #000000;
- border: 2px solid #FFFFFF;
-}
-
-QTabBar::tab:disabled {
- /* a tab that is disabled */
- background: #EBEBEB;
- color: #808080;
- border: 2px solid transparent;
-}
-
-QTabBar::tab:selected {
- /* a tab that is clicked */
- color: #FFFFFF;
- background: #008484;
- border: 2px solid #008484;
-}
-
-QTabBar::tab:hover {
- /* a tab when moused-over */
- background: #C2C2C2;
- color: #000000;
- border: 2px solid #C2C2C2;
-}
-
-QTabBar::tab:first {
- /* that first tab */
- border-top-left-radius: 10px;
- border-bottom-left-radius: 10px;
-}
-
-QTabBar::tab:last {
- /* the last tab */
- border-top-right-radius: 10px;
- border-bottom-right-radius: 10px;
-}
-
-QTabBar QToolButton {
- /* buttons to scroll between more tabs on a tab bar */
- background: #C2C2C2;
- padding: 1px;
- border-radius: 6px;
- margin: 1px;
-}
-
-QTabBar QToolButton:disabled {
- /* buttons to scroll on a tab bar when it's unavailable */
- background: transparent;
-}
-
-QLCDNumber {
- /* LCD number on the Conflicts tab */
- background: #FFFFFF;
- color: #008484;
- border-radius: 6px;
-}
-
-/* Tables (Configure Mod Categories) */
-
-QTableView {
- /* a table */
- gridline-color: #EBEBEB;
- border: 0px;
-}
-
-/* Checkboxes */
-
-QTreeView::indicator:unchecked,
-QCheckBox::indicator:unchecked,
-QGroupBox::indicator:unchecked,
-QRadioButton::indicator:unchecked {
- /* a checkbox that is unchecked */
- image: url(./Paper/Light/unchecked.svg);
- width: 14px;
- height: 14px;
-}
-
-QCheckBox::indicator:unchecked:hover,
-QRadioButton::indicator:unchecked:hover {
- /* a checkbox that is unchecked when moused-over and clicked */
- image: url(./Paper/Light/unchecked-hover.svg);
-}
-
-QTreeView::indicator:unchecked:selected {
- image: url(./Paper/Light/unchecked-alt.svg);
-}
-
-QTreeView::indicator:checked,
-QCheckBox::indicator:checked,
-QGroupBox::indicator:checked,
-QRadioButton::indicator:checked {
- /* a checkbox that is checked */
- image: url(./Paper/Light/check.svg);
- width: 14px;
- height: 14px;
-}
-
-QCheckBox::indicator:checked:hover,
-QRadioButton::indicator:checked:hover {
- /* a checkbox that is checked when moused-over and clicked */
- image: url(./Paper/Light/check-alt.svg);
-}
-
-QTreeView::indicator:checked:selected {
- image: url(./Paper/Light/check-white.svg);
-}
-
-QCheckBox::indicator:disabled {
- /* a checkbox that is disabled */
- image: url(./Paper/Light/unchecked-disabled.svg);
-}
-
-/* Spinboxes */
-
-QSpinBox,
-QDoubleSpinBox {
- /* usually boxes for selecting numbers */
- min-height: 24px;
- min-width: 60px;
- background: #FFFFFF;
- padding: 0px 2px;
- border: 2px solid #EBEBEB;
- border-radius: 6px;
- margin: 0px -4px;
-}
-
-QSpinBox::up-button,
-QDoubleSpinBox::up-button {
- /* up button on spinboxes */
- min-height: 28px;
- min-width: 20px;
- subcontrol-position: center right;
- border: 2px solid #EBEBEB;
- border-top-right-radius: 6px;
- border-bottom-right-radius: 6px;
-}
-
-QSpinBox::up-arrow,
-QDoubleSpinBox::up-arrow {
- /* arrow for the up button */
- image: url(./Paper/Light/Arrows/up.svg);
-}
-
-QSpinBox::up-button:hover,
-QDoubleSpinBox::up-button:hover {
- /* up button on spinboxes when moused-over */
- background: #C2C2C2;
-}
-
-QSpinBox::down-button,
-QDoubleSpinBox::down-button {
- /* down button on spinboxes */
- min-height: 28px;
- min-width: 20px;
- subcontrol-position: center left;
- border: 2px solid #EBEBEB;
- border-top-left-radius: 6px;
- border-bottom-left-radius: 6px;
-}
-
-QSpinBox::down-arrow,
-QDoubleSpinBox::down-arrow {
- /* arrow for the up button */
- image: url(./Paper/Light/Arrows/down.svg);
-}
-
-QSpinBox::down-button:hover,
-QDoubleSpinBox::down-button:hover {
- /* down button on spinboxes when moused-over */
- background: #C2C2C2;
-}
-
-/* Sliders */
-
-QSlider::groove {
- /* sliders */
- height: 0px;
- border: 1px solid #008484;
-}
-
-QSlider::handle {
- /* slider handles */
- background: #FFFFFF;
- border: 2px solid #008484;
- border-radius: 6px;
- margin: -10px;
-}
-
-QSlider::handle:hover {
- /* Slider handles when moused-over */
- background: #008484;
-}
-
-/* Pre-v2.1.7 Downloads Tab */
-
-#downloadTab QAbstractScrollArea {
- /* background of the entire downloads tab */
- background: #FFFFFF;
-}
-
-DownloadListWidget QFrame,
-DownloadListWidgetCompact,
-DownloadListWidgetCompact QLabel {
- /* an entry on the Downloads tab */
- background: #FFFFFF;
-}
-
-DownloadListWidget #frame {
- /* outer box of an entry on the Downloads tab */
- border: none;
-}
-
-#installLabel {
- /* installed/done label */
- color: none;
-}
-
-DownloadListWidgetCompact,
-DownloadListWidgetCompact QLabel {
- /* an entry on the Downloads tab in Compacts View*/
- background: #FFFFFF;
-}
-
-/* New Downloads View */
-
-DownloadListWidget[downloadView=standard]::item {
- /* an entry on the Downloads view */
- min-height: 44px;
-}
-
-QProgressBar[downloadView=standard] {
- /* a progress bar on the Downloads view */
- margin: 11px 0;
-}
-
-/* Categories Filter */
-
-#displayCategoriesBtn {
- /* Filter button */
- min-width: 12px;
-}
-
-#categoriesList {
- /* Categories panel */
- min-width: 200px;
- margin-bottom: 4px;
-}
-
-#categoriesGroup {
- /* Categories group box */
- padding-bottom: 0px;
-}
-
-/* Fixes */
-
-#executablesListBox {
- /* Increase right margin of the select executables box */
- margin-right: 8px;
-}
-
-#stepsStack QWidget {
- /* Groupboxes on the FOMOD Installer Dialog */
- border: none;
-}
-
-#stepsStack QGroupBox {
- /* Fix to reimplement styling for Groupboxes on the FOMOD Installer dialog */
- border: 2px solid #EBEBEB;
- border-radius: 10px;
-}
-
-#activeModslabel, #activePluginsLabel {
- /* Increase the left margin of the counters */
- padding-left: 6px;
-}
diff --git a/src/stylesheets/Paper/Automata/Arrows/down.svg b/src/stylesheets/Paper/Automata/Arrows/down.svg
deleted file mode 100644
index 30f13836..00000000
--- a/src/stylesheets/Paper/Automata/Arrows/down.svg
+++ /dev/null
@@ -1,97 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
- xmlns:dc="http://purl.org/dc/elements/1.1/"
- xmlns:cc="http://creativecommons.org/ns#"
- xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
- xmlns:svg="http://www.w3.org/2000/svg"
- xmlns="http://www.w3.org/2000/svg"
- xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
- xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
- width="8"
- height="8"
- viewBox="0 0 8 8"
- version="1.1"
- id="svg8"
- inkscape:export-filename="D:\Bethesda Game Modding\Mod Organizer 2\stylesheets\Paper\Dark\dots.png"
- inkscape:export-xdpi="307.20001"
- inkscape:export-ydpi="307.20001"
- inkscape:version="0.92.1 r15371"
- sodipodi:docname="down.svg">
- <defs
- id="defs2" />
- <sodipodi:namedview
- id="base"
- pagecolor="#505050"
- bordercolor="#666666"
- borderopacity="1.0"
- inkscape:pageopacity="0"
- inkscape:pageshadow="2"
- inkscape:zoom="116.25"
- inkscape:cx="4"
- inkscape:cy="4"
- inkscape:document-units="px"
- inkscape:current-layer="layer25"
- showgrid="false"
- units="px"
- borderlayer="true"
- inkscape:showpageshadow="false"
- inkscape:snap-page="true"
- inkscape:snap-bbox="true"
- inkscape:bbox-nodes="true"
- inkscape:snap-bbox-edge-midpoints="true"
- inkscape:snap-bbox-midpoints="true"
- showguides="true"
- inkscape:guide-bbox="true"
- inkscape:window-width="1920"
- inkscape:window-height="1027"
- inkscape:window-x="-8"
- inkscape:window-y="-8"
- inkscape:window-maximized="1"
- inkscape:snap-center="true"
- inkscape:snap-object-midpoints="true"
- inkscape:measure-start="18.8975,12.498"
- inkscape:measure-end="20,12.498"
- inkscape:snap-intersection-paths="true"
- inkscape:object-paths="true"
- inkscape:snap-smooth-nodes="true"
- inkscape:snap-midpoints="true"
- inkscape:snap-global="true">
- <sodipodi:guide
- position="4,4"
- orientation="1,0"
- id="guide4039"
- inkscape:locked="false" />
- <sodipodi:guide
- position="4,4"
- orientation="0,1"
- id="guide3935"
- inkscape:locked="false"
- inkscape:label=""
- inkscape:color="rgb(0,0,255)" />
- </sodipodi:namedview>
- <metadata
- id="metadata5">
- <rdf:RDF>
- <cc:Work
- rdf:about="">
- <dc:format>image/svg+xml</dc:format>
- <dc:type
- rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
- <dc:title />
- </cc:Work>
- </rdf:RDF>
- </metadata>
- <g
- inkscape:groupmode="layer"
- id="layer25"
- inkscape:label="Content"
- transform="translate(0,-12)">
- <path
- style="display:inline;opacity:1;fill:#4d4a41;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:212.1320343;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:1.93939996;stroke-opacity:1;paint-order:markers stroke fill"
- d="M 0,15 2,17 4,19 6,17 8,15 6,13 4,15 2,13 Z"
- id="rect3900-0"
- inkscape:connector-curvature="0" />
- </g>
-</svg>
diff --git a/src/stylesheets/Paper/Automata/Arrows/left.svg b/src/stylesheets/Paper/Automata/Arrows/left.svg
deleted file mode 100644
index 97094916..00000000
--- a/src/stylesheets/Paper/Automata/Arrows/left.svg
+++ /dev/null
@@ -1,97 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
- xmlns:dc="http://purl.org/dc/elements/1.1/"
- xmlns:cc="http://creativecommons.org/ns#"
- xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
- xmlns:svg="http://www.w3.org/2000/svg"
- xmlns="http://www.w3.org/2000/svg"
- xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
- xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
- width="8"
- height="8"
- viewBox="0 0 8 8"
- version="1.1"
- id="svg8"
- inkscape:export-filename="D:\Bethesda Game Modding\Mod Organizer 2\stylesheets\Paper\Dark\dots.png"
- inkscape:export-xdpi="307.20001"
- inkscape:export-ydpi="307.20001"
- inkscape:version="0.92.1 r15371"
- sodipodi:docname="left.svg">
- <defs
- id="defs2" />
- <sodipodi:namedview
- id="base"
- pagecolor="#505050"
- bordercolor="#666666"
- borderopacity="1.0"
- inkscape:pageopacity="0"
- inkscape:pageshadow="2"
- inkscape:zoom="116.25"
- inkscape:cx="4"
- inkscape:cy="4"
- inkscape:document-units="px"
- inkscape:current-layer="layer25"
- showgrid="false"
- units="px"
- borderlayer="true"
- inkscape:showpageshadow="false"
- inkscape:snap-page="true"
- inkscape:snap-bbox="true"
- inkscape:bbox-nodes="true"
- inkscape:snap-bbox-edge-midpoints="true"
- inkscape:snap-bbox-midpoints="true"
- showguides="true"
- inkscape:guide-bbox="true"
- inkscape:window-width="1920"
- inkscape:window-height="1027"
- inkscape:window-x="-8"
- inkscape:window-y="-8"
- inkscape:window-maximized="1"
- inkscape:snap-center="true"
- inkscape:snap-object-midpoints="true"
- inkscape:measure-start="18.8975,12.498"
- inkscape:measure-end="20,12.498"
- inkscape:snap-intersection-paths="true"
- inkscape:object-paths="true"
- inkscape:snap-smooth-nodes="true"
- inkscape:snap-midpoints="true"
- inkscape:snap-global="true">
- <sodipodi:guide
- position="4,4"
- orientation="1,0"
- id="guide4039"
- inkscape:locked="false" />
- <sodipodi:guide
- position="4,4"
- orientation="0,1"
- id="guide3935"
- inkscape:locked="false"
- inkscape:label=""
- inkscape:color="rgb(0,0,255)" />
- </sodipodi:namedview>
- <metadata
- id="metadata5">
- <rdf:RDF>
- <cc:Work
- rdf:about="">
- <dc:format>image/svg+xml</dc:format>
- <dc:type
- rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
- <dc:title />
- </cc:Work>
- </rdf:RDF>
- </metadata>
- <g
- inkscape:groupmode="layer"
- id="layer25"
- inkscape:label="Content"
- transform="translate(0,-12)">
- <path
- style="display:inline;opacity:1;fill:#4d4a41;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:212.1320343;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:1.93939996;stroke-opacity:1;paint-order:markers stroke fill"
- d="M 5,20 3,18 1,16 3,14 5,12 7,14 5,16 7,18 Z"
- id="rect3900-0"
- inkscape:connector-curvature="0" />
- </g>
-</svg>
diff --git a/src/stylesheets/Paper/Automata/Arrows/right.svg b/src/stylesheets/Paper/Automata/Arrows/right.svg
deleted file mode 100644
index 5eaa99cf..00000000
--- a/src/stylesheets/Paper/Automata/Arrows/right.svg
+++ /dev/null
@@ -1,97 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
- xmlns:dc="http://purl.org/dc/elements/1.1/"
- xmlns:cc="http://creativecommons.org/ns#"
- xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
- xmlns:svg="http://www.w3.org/2000/svg"
- xmlns="http://www.w3.org/2000/svg"
- xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
- xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
- width="8"
- height="8"
- viewBox="0 0 8 8"
- version="1.1"
- id="svg8"
- inkscape:export-filename="D:\Bethesda Game Modding\Mod Organizer 2\stylesheets\Paper\Dark\dots.png"
- inkscape:export-xdpi="307.20001"
- inkscape:export-ydpi="307.20001"
- inkscape:version="0.92.1 r15371"
- sodipodi:docname="right.svg">
- <defs
- id="defs2" />
- <sodipodi:namedview
- id="base"
- pagecolor="#505050"
- bordercolor="#666666"
- borderopacity="1.0"
- inkscape:pageopacity="0"
- inkscape:pageshadow="2"
- inkscape:zoom="116.25"
- inkscape:cx="4"
- inkscape:cy="4"
- inkscape:document-units="px"
- inkscape:current-layer="layer25"
- showgrid="false"
- units="px"
- borderlayer="true"
- inkscape:showpageshadow="false"
- inkscape:snap-page="true"
- inkscape:snap-bbox="true"
- inkscape:bbox-nodes="true"
- inkscape:snap-bbox-edge-midpoints="true"
- inkscape:snap-bbox-midpoints="true"
- showguides="true"
- inkscape:guide-bbox="true"
- inkscape:window-width="1920"
- inkscape:window-height="1027"
- inkscape:window-x="-8"
- inkscape:window-y="-8"
- inkscape:window-maximized="1"
- inkscape:snap-center="true"
- inkscape:snap-object-midpoints="true"
- inkscape:measure-start="18.8975,12.498"
- inkscape:measure-end="20,12.498"
- inkscape:snap-intersection-paths="true"
- inkscape:object-paths="true"
- inkscape:snap-smooth-nodes="true"
- inkscape:snap-midpoints="true"
- inkscape:snap-global="true">
- <sodipodi:guide
- position="4,4"
- orientation="1,0"
- id="guide4039"
- inkscape:locked="false" />
- <sodipodi:guide
- position="4,4"
- orientation="0,1"
- id="guide3935"
- inkscape:locked="false"
- inkscape:label=""
- inkscape:color="rgb(0,0,255)" />
- </sodipodi:namedview>
- <metadata
- id="metadata5">
- <rdf:RDF>
- <cc:Work
- rdf:about="">
- <dc:format>image/svg+xml</dc:format>
- <dc:type
- rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
- <dc:title />
- </cc:Work>
- </rdf:RDF>
- </metadata>
- <g
- inkscape:groupmode="layer"
- id="layer25"
- inkscape:label="Content"
- transform="translate(0,-12)">
- <path
- style="display:inline;opacity:1;fill:#4d4a41;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:212.1320343;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:1.93939996;stroke-opacity:1;paint-order:markers stroke fill"
- d="M 3,20 5,18 7,16 5,14 3,12 1,14 3,16 1,18 Z"
- id="rect3900-0"
- inkscape:connector-curvature="0" />
- </g>
-</svg>
diff --git a/src/stylesheets/Paper/Automata/Arrows/up.svg b/src/stylesheets/Paper/Automata/Arrows/up.svg
deleted file mode 100644
index 5fa569f4..00000000
--- a/src/stylesheets/Paper/Automata/Arrows/up.svg
+++ /dev/null
@@ -1,97 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
- xmlns:dc="http://purl.org/dc/elements/1.1/"
- xmlns:cc="http://creativecommons.org/ns#"
- xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
- xmlns:svg="http://www.w3.org/2000/svg"
- xmlns="http://www.w3.org/2000/svg"
- xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
- xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
- width="8"
- height="8"
- viewBox="0 0 8 8"
- version="1.1"
- id="svg8"
- inkscape:export-filename="D:\Bethesda Game Modding\Mod Organizer 2\stylesheets\Paper\Dark\dots.png"
- inkscape:export-xdpi="307.20001"
- inkscape:export-ydpi="307.20001"
- inkscape:version="0.92.1 r15371"
- sodipodi:docname="up.svg">
- <defs
- id="defs2" />
- <sodipodi:namedview
- id="base"
- pagecolor="#505050"
- bordercolor="#666666"
- borderopacity="1.0"
- inkscape:pageopacity="0"
- inkscape:pageshadow="2"
- inkscape:zoom="116.25"
- inkscape:cx="4"
- inkscape:cy="4"
- inkscape:document-units="px"
- inkscape:current-layer="layer25"
- showgrid="false"
- units="px"
- borderlayer="true"
- inkscape:showpageshadow="false"
- inkscape:snap-page="true"
- inkscape:snap-bbox="true"
- inkscape:bbox-nodes="true"
- inkscape:snap-bbox-edge-midpoints="true"
- inkscape:snap-bbox-midpoints="true"
- showguides="true"
- inkscape:guide-bbox="true"
- inkscape:window-width="1920"
- inkscape:window-height="1027"
- inkscape:window-x="-8"
- inkscape:window-y="-8"
- inkscape:window-maximized="1"
- inkscape:snap-center="true"
- inkscape:snap-object-midpoints="true"
- inkscape:measure-start="18.8975,12.498"
- inkscape:measure-end="20,12.498"
- inkscape:snap-intersection-paths="true"
- inkscape:object-paths="true"
- inkscape:snap-smooth-nodes="true"
- inkscape:snap-midpoints="true"
- inkscape:snap-global="true">
- <sodipodi:guide
- position="4,4"
- orientation="1,0"
- id="guide4039"
- inkscape:locked="false" />
- <sodipodi:guide
- position="4,4"
- orientation="0,1"
- id="guide3935"
- inkscape:locked="false"
- inkscape:label=""
- inkscape:color="rgb(0,0,255)" />
- </sodipodi:namedview>
- <metadata
- id="metadata5">
- <rdf:RDF>
- <cc:Work
- rdf:about="">
- <dc:format>image/svg+xml</dc:format>
- <dc:type
- rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
- <dc:title />
- </cc:Work>
- </rdf:RDF>
- </metadata>
- <g
- inkscape:groupmode="layer"
- id="layer25"
- inkscape:label="Content"
- transform="translate(0,-12)">
- <path
- style="display:inline;opacity:1;fill:#4d4a41;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:212.1320343;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:1.93939996;stroke-opacity:1;paint-order:markers stroke fill"
- d="m 0,17 2,-2 2,-2 2,2 2,2 -2,2 -2,-2 -2,2 z"
- id="rect3900-0"
- inkscape:connector-curvature="0" />
- </g>
-</svg>
diff --git a/src/stylesheets/Paper/Automata/Toolbar/archives.svg b/src/stylesheets/Paper/Automata/Toolbar/archives.svg
deleted file mode 100644
index c13182c9..00000000
--- a/src/stylesheets/Paper/Automata/Toolbar/archives.svg
+++ /dev/null
@@ -1,99 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
- xmlns:dc="http://purl.org/dc/elements/1.1/"
- xmlns:cc="http://creativecommons.org/ns#"
- xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
- xmlns:svg="http://www.w3.org/2000/svg"
- xmlns="http://www.w3.org/2000/svg"
- xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
- xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
- width="128"
- height="128"
- viewBox="0 0 128 128"
- version="1.1"
- id="svg8"
- inkscape:export-filename="D:\Bethesda Game Modding\Mod Organizer 2\stylesheets\Paper\Dark\dots.png"
- inkscape:export-xdpi="307.20001"
- inkscape:export-ydpi="307.20001"
- inkscape:version="0.92.1 r15371"
- sodipodi:docname="archives.svg">
- <defs
- id="defs2" />
- <sodipodi:namedview
- id="base"
- pagecolor="#505050"
- bordercolor="#666666"
- borderopacity="1.0"
- inkscape:pageopacity="0"
- inkscape:pageshadow="2"
- inkscape:zoom="8.1349342"
- inkscape:cx="95.872503"
- inkscape:cy="56.896625"
- inkscape:document-units="px"
- inkscape:current-layer="layer25"
- showgrid="false"
- units="px"
- borderlayer="true"
- inkscape:showpageshadow="false"
- inkscape:snap-page="true"
- inkscape:snap-bbox="true"
- inkscape:bbox-nodes="true"
- inkscape:snap-bbox-edge-midpoints="true"
- inkscape:snap-bbox-midpoints="true"
- showguides="true"
- inkscape:guide-bbox="true"
- inkscape:window-width="1920"
- inkscape:window-height="1027"
- inkscape:window-x="-8"
- inkscape:window-y="-8"
- inkscape:window-maximized="1"
- inkscape:snap-center="true"
- inkscape:snap-object-midpoints="true"
- inkscape:measure-start="18.8975,12.498"
- inkscape:measure-end="20,12.498"
- inkscape:snap-intersection-paths="true"
- inkscape:object-paths="true"
- inkscape:snap-smooth-nodes="true"
- inkscape:snap-midpoints="true"
- inkscape:snap-global="true"
- inkscape:lockguides="true">
- <sodipodi:guide
- position="64,64"
- orientation="1,0"
- id="guide4039"
- inkscape:locked="true" />
- <sodipodi:guide
- position="64,64"
- orientation="0,1"
- id="guide3935"
- inkscape:locked="true"
- inkscape:label=""
- inkscape:color="rgb(0,0,255)" />
- </sodipodi:namedview>
- <metadata
- id="metadata5">
- <rdf:RDF>
- <cc:Work
- rdf:about="">
- <dc:format>image/svg+xml</dc:format>
- <dc:type
- rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
- <dc:title />
- </cc:Work>
- </rdf:RDF>
- </metadata>
- <g
- inkscape:groupmode="layer"
- id="layer25"
- inkscape:label="Content"
- transform="translate(0,108)">
- <path
- style="opacity:1;fill:#4e4b42;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:8;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:normal"
- d="m 73.998642,-83.999999 -9.998644,10.001356 H 14.001355 V -4.000001 H 113.99864 V -73.998643 L 104,-83.999999 H 88.999315 Z"
- id="path3849"
- inkscape:connector-curvature="0"
- sodipodi:nodetypes="ccccccccc" />
- </g>
-</svg>
diff --git a/src/stylesheets/Paper/Automata/Toolbar/executables.svg b/src/stylesheets/Paper/Automata/Toolbar/executables.svg
deleted file mode 100644
index 6c442f76..00000000
--- a/src/stylesheets/Paper/Automata/Toolbar/executables.svg
+++ /dev/null
@@ -1,99 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
- xmlns:dc="http://purl.org/dc/elements/1.1/"
- xmlns:cc="http://creativecommons.org/ns#"
- xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
- xmlns:svg="http://www.w3.org/2000/svg"
- xmlns="http://www.w3.org/2000/svg"
- xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
- xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
- width="128"
- height="128"
- viewBox="0 0 128 128"
- version="1.1"
- id="svg8"
- inkscape:export-filename="D:\Bethesda Game Modding\Mod Organizer 2\stylesheets\Paper\Dark\dots.png"
- inkscape:export-xdpi="307.20001"
- inkscape:export-ydpi="307.20001"
- inkscape:version="0.92.1 r15371"
- sodipodi:docname="executables.svg">
- <defs
- id="defs2" />
- <sodipodi:namedview
- id="base"
- pagecolor="#505050"
- bordercolor="#666666"
- borderopacity="1.0"
- inkscape:pageopacity="0"
- inkscape:pageshadow="2"
- inkscape:zoom="3.100169"
- inkscape:cx="188.48486"
- inkscape:cy="-11.491737"
- inkscape:document-units="px"
- inkscape:current-layer="layer25"
- showgrid="false"
- units="px"
- borderlayer="true"
- inkscape:showpageshadow="false"
- inkscape:snap-page="true"
- inkscape:snap-bbox="true"
- inkscape:bbox-nodes="true"
- inkscape:snap-bbox-edge-midpoints="true"
- inkscape:snap-bbox-midpoints="true"
- showguides="true"
- inkscape:guide-bbox="true"
- inkscape:window-width="1920"
- inkscape:window-height="1027"
- inkscape:window-x="-8"
- inkscape:window-y="-8"
- inkscape:window-maximized="1"
- inkscape:snap-center="true"
- inkscape:snap-object-midpoints="true"
- inkscape:measure-start="18.8975,12.498"
- inkscape:measure-end="20,12.498"
- inkscape:snap-intersection-paths="true"
- inkscape:object-paths="true"
- inkscape:snap-smooth-nodes="true"
- inkscape:snap-midpoints="true"
- inkscape:snap-global="true">
- <sodipodi:guide
- position="64,64"
- orientation="1,0"
- id="guide4039"
- inkscape:locked="false" />
- <sodipodi:guide
- position="64,64"
- orientation="0,1"
- id="guide3935"
- inkscape:locked="false"
- inkscape:label=""
- inkscape:color="rgb(0,0,255)" />
- </sodipodi:namedview>
- <metadata
- id="metadata5">
- <rdf:RDF>
- <cc:Work
- rdf:about="">
- <dc:format>image/svg+xml</dc:format>
- <dc:type
- rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
- <dc:title />
- </cc:Work>
- </rdf:RDF>
- </metadata>
- <g
- inkscape:groupmode="layer"
- id="layer25"
- inkscape:label="Content"
- transform="translate(0,108)">
- <path
- inkscape:connector-curvature="0"
- style="display:inline;opacity:1;fill:#4e4b42;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:3.00000048;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:stroke fill markers"
- d="m 69.224291,-99 v 25.297161 A 57.019516,45.615413 0 0 0 12.206203,-28.088371 57.019516,45.615413 0 0 0 40.038762,11 h 7.071835 A 34.069164,27.255211 0 0 1 35.161239,-9.7379829 34.069164,27.255211 0 0 1 69.224291,-36.979329 v 25.297161 L 115.7938,-55.341082 Z"
- id="rect3929"
- inkscape:transform-center-x="-6.418094"
- inkscape:transform-center-y="3.2118799" />
- </g>
-</svg>
diff --git a/src/stylesheets/Paper/Automata/Toolbar/help.svg b/src/stylesheets/Paper/Automata/Toolbar/help.svg
deleted file mode 100644
index 2cab3931..00000000
--- a/src/stylesheets/Paper/Automata/Toolbar/help.svg
+++ /dev/null
@@ -1,128 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
- xmlns:dc="http://purl.org/dc/elements/1.1/"
- xmlns:cc="http://creativecommons.org/ns#"
- xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
- xmlns:svg="http://www.w3.org/2000/svg"
- xmlns="http://www.w3.org/2000/svg"
- xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
- xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
- width="128"
- height="128"
- viewBox="0 0 128 128"
- version="1.1"
- id="svg8"
- inkscape:export-filename="D:\Bethesda Game Modding\Mod Organizer 2\stylesheets\Paper\Dark\dots.png"
- inkscape:export-xdpi="307.20001"
- inkscape:export-ydpi="307.20001"
- inkscape:version="0.92.1 r15371"
- sodipodi:docname="help.svg">
- <defs
- id="defs2" />
- <sodipodi:namedview
- id="base"
- pagecolor="#505050"
- bordercolor="#666666"
- borderopacity="1.0"
- inkscape:pageopacity="0"
- inkscape:pageshadow="2"
- inkscape:zoom="4.7258155"
- inkscape:cx="76.464409"
- inkscape:cy="41.213613"
- inkscape:document-units="px"
- inkscape:current-layer="layer25"
- showgrid="false"
- units="px"
- borderlayer="true"
- inkscape:showpageshadow="false"
- inkscape:snap-page="true"
- inkscape:snap-bbox="true"
- inkscape:bbox-nodes="true"
- inkscape:snap-bbox-edge-midpoints="true"
- inkscape:snap-bbox-midpoints="true"
- showguides="true"
- inkscape:guide-bbox="true"
- inkscape:window-width="1920"
- inkscape:window-height="1027"
- inkscape:window-x="-8"
- inkscape:window-y="-8"
- inkscape:window-maximized="1"
- inkscape:snap-center="true"
- inkscape:snap-object-midpoints="true"
- inkscape:measure-start="18.8975,12.498"
- inkscape:measure-end="20,12.498"
- inkscape:snap-intersection-paths="true"
- inkscape:object-paths="true"
- inkscape:snap-smooth-nodes="true"
- inkscape:snap-midpoints="true"
- inkscape:snap-global="true">
- <sodipodi:guide
- position="64,64"
- orientation="1,0"
- id="guide4039"
- inkscape:locked="false" />
- <sodipodi:guide
- position="64,64"
- orientation="0,1"
- id="guide3935"
- inkscape:locked="false"
- inkscape:label=""
- inkscape:color="rgb(0,0,255)" />
- </sodipodi:namedview>
- <metadata
- id="metadata5">
- <rdf:RDF>
- <cc:Work
- rdf:about="">
- <dc:format>image/svg+xml</dc:format>
- <dc:type
- rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
- <dc:title />
- </cc:Work>
- </rdf:RDF>
- </metadata>
- <g
- inkscape:groupmode="layer"
- id="layer25"
- inkscape:label="Content"
- transform="translate(0,108)">
- <circle
- style="opacity:1;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:#4e4b42;stroke-width:8;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:stroke fill markers"
- id="path3848"
- cx="64"
- cy="-44"
- r="60" />
- <g
- id="g3871"
- transform="translate(0,-1.242582)"
- style="stroke-width:1;fill:#4e4b42;fill-opacity:1">
- <path
- id="path3851"
- d="m 64,-80 c -4.113298,1.67e-4 -7.447744,3.334612 -7.44791,7.44791 -6.7e-4,4.11389 3.33402,7.449258 7.44791,7.449426 4.11389,-1.68e-4 7.44858,-3.335536 7.44791,-7.449426 C 71.447743,-76.665387 68.113298,-79.999833 64,-80 Z"
- style="opacity:1;fill:#4e4b42;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:8;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:stroke fill markers"
- inkscape:connector-curvature="0" />
- <path
- inkscape:connector-curvature="0"
- id="path3860"
- d="m 56.55209,-50.206086 v 34.757418 h 14.89582 v -34.757418 z"
- style="opacity:1;fill:#4e4b42;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:8;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:stroke fill markers" />
- <path
- style="opacity:1;fill:#4e4b42;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:8;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:stroke fill markers"
- d="m 64,-80 c -4.113298,1.67e-4 -7.447744,3.334612 -7.44791,7.44791 -6.7e-4,4.11389 3.33402,7.449258 7.44791,7.449426 4.11389,-1.68e-4 7.44858,-3.335536 7.44791,-7.449426 C 71.447743,-76.665387 68.113298,-79.999833 64,-80 Z"
- id="path3853"
- inkscape:connector-curvature="0" />
- <path
- inkscape:connector-curvature="0"
- id="path3862"
- d="m 64,-57.654754 c -4.113298,1.67e-4 -7.447744,3.334612 -7.44791,7.44791 -6.7e-4,4.11389 3.33402,7.449258 7.44791,7.449426 4.11389,-1.68e-4 7.44858,-3.335536 7.44791,-7.449426 -1.67e-4,-4.113297 -3.334612,-7.447743 -7.44791,-7.44791 z"
- style="opacity:1;fill:#4e4b42;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:8;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:stroke fill markers" />
- <path
- style="opacity:1;fill:#4e4b42;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:8;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:stroke fill markers"
- d="m 64,-22.897336 c -4.113298,1.67e-4 -7.447744,3.334612 -7.44791,7.44791 C 56.55142,-11.335536 59.88611,-8.000168 64,-8 c 4.11389,-1.68e-4 7.44858,-3.335536 7.44791,-7.449426 -1.67e-4,-4.113297 -3.334612,-7.447743 -7.44791,-7.44791 z"
- id="path3864"
- inkscape:connector-curvature="0" />
- </g>
- </g>
-</svg>
diff --git a/src/stylesheets/Paper/Automata/Toolbar/instances.svg b/src/stylesheets/Paper/Automata/Toolbar/instances.svg
deleted file mode 100644
index 4b0cd707..00000000
--- a/src/stylesheets/Paper/Automata/Toolbar/instances.svg
+++ /dev/null
@@ -1,98 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
- xmlns:dc="http://purl.org/dc/elements/1.1/"
- xmlns:cc="http://creativecommons.org/ns#"
- xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
- xmlns:svg="http://www.w3.org/2000/svg"
- xmlns="http://www.w3.org/2000/svg"
- xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
- xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
- width="128"
- height="128"
- viewBox="0 0 128 128"
- version="1.1"
- id="svg8"
- inkscape:export-filename="D:\Bethesda Game Modding\Mod Organizer 2\stylesheets\Paper\Dark\dots.png"
- inkscape:export-xdpi="307.20001"
- inkscape:export-ydpi="307.20001"
- inkscape:version="0.92.1 r15371"
- sodipodi:docname="instances.svg">
- <defs
- id="defs2" />
- <sodipodi:namedview
- id="base"
- pagecolor="#505050"
- bordercolor="#666666"
- borderopacity="1.0"
- inkscape:pageopacity="0"
- inkscape:pageshadow="2"
- inkscape:zoom="3.7735291"
- inkscape:cx="64.04823"
- inkscape:cy="-33.258287"
- inkscape:document-units="px"
- inkscape:current-layer="layer25"
- showgrid="false"
- units="px"
- borderlayer="true"
- inkscape:showpageshadow="false"
- inkscape:snap-page="true"
- inkscape:snap-bbox="true"
- inkscape:bbox-nodes="true"
- inkscape:snap-bbox-edge-midpoints="true"
- inkscape:snap-bbox-midpoints="true"
- showguides="true"
- inkscape:guide-bbox="true"
- inkscape:window-width="1920"
- inkscape:window-height="1027"
- inkscape:window-x="-8"
- inkscape:window-y="-8"
- inkscape:window-maximized="1"
- inkscape:snap-center="true"
- inkscape:snap-object-midpoints="true"
- inkscape:measure-start="18.8975,12.498"
- inkscape:measure-end="20,12.498"
- inkscape:snap-intersection-paths="true"
- inkscape:object-paths="true"
- inkscape:snap-smooth-nodes="true"
- inkscape:snap-midpoints="true"
- inkscape:snap-global="true">
- <sodipodi:guide
- position="64,64"
- orientation="1,0"
- id="guide4039"
- inkscape:locked="false" />
- <sodipodi:guide
- position="64,64"
- orientation="0,1"
- id="guide3935"
- inkscape:locked="false"
- inkscape:label=""
- inkscape:color="rgb(0,0,255)" />
- </sodipodi:namedview>
- <metadata
- id="metadata5">
- <rdf:RDF>
- <cc:Work
- rdf:about="">
- <dc:format>image/svg+xml</dc:format>
- <dc:type
- rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
- <dc:title />
- </cc:Work>
- </rdf:RDF>
- </metadata>
- <g
- inkscape:groupmode="layer"
- id="layer25"
- inkscape:label="Content"
- transform="translate(0,108)">
- <path
- style="opacity:1;fill:#4e4b42;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:8;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:normal"
- d="m 43.074486,-84 c -7.191305,0.179697 -13.681107,2.360737 -19.604836,6.184844 l -0.829325,3.561678 -4.167425,3.208112 -5.581697,14.982451 c -5.7982022,15.544107 -18.3828084,44.900541 4.037437,52.0629145 5.467067,-4.389774 11.744564,-10.4769935 18.039776,-16.6021055 3.729503,-3.62877 7.012953,-4.081632 14.160925,-4.081632 h 14.865463 14.865462 c 7.147976,0 10.431426,0.452872 14.160926,4.081632 6.2952,6.125112 12.583088,12.2123315 18.050168,16.6021055 22.42024,-7.1623735 9.83564,-36.5188075 4.03744,-52.0629145 l -5.5921,-14.982451 -4.15702,-3.208112 -0.82933,-3.561678 C 98.606622,-81.639263 92.106422,-83.820123 84.915122,-84 l -5.46211,2.979332 H 63.994804 48.546994 Z m 51.652146,11.766541 c 2.28943,-7.75e-4 4.14597,1.854604 4.14663,4.144028 7.8e-4,2.290441 -1.85618,4.147401 -4.14663,4.146627 -2.28942,-6.62e-4 -4.1448,-1.857203 -4.14402,-4.146627 6.6e-4,-2.288409 1.85562,-4.143366 4.14402,-4.144028 z m -61.453263,4.144028 c 4.578848,-1.12e-4 8.290765,3.711805 8.290653,8.290653 1.13e-4,4.578848 -3.711805,8.290766 -8.290653,8.290655 -4.578848,1.13e-4 -8.290768,-3.711807 -8.290654,-8.290655 -1.12e-4,-4.578848 3.711806,-8.290766 8.290654,-8.290653 z m 53.162613,4.146627 c 2.28943,-7.75e-4 4.14597,1.854603 4.14663,4.144026 7.7e-4,2.290441 -1.85619,4.147403 -4.14663,4.146627 -2.289423,-6.62e-4 -4.144799,-1.857203 -4.144025,-4.146627 6.62e-4,-2.288409 1.855618,-4.143365 4.144025,-4.144026 z m 16.581308,0 c 2.28942,-7.75e-4 4.14596,1.854603 4.14663,4.144026 7.7e-4,2.290441 -1.85619,4.147403 -4.14663,4.146627 -2.28943,-6.62e-4 -4.144798,-1.857203 -4.144028,-4.146627 6.6e-4,-2.288409 1.855618,-4.143365 4.144028,-4.144026 z m -8.290658,8.290654 c 2.28943,-7.75e-4 4.14597,1.854602 4.14663,4.144027 7.8e-4,2.29044 -1.85618,4.147401 -4.14663,4.146626 -2.28942,-6.62e-4 -4.1448,-1.857203 -4.14402,-4.146626 6.6e-4,-2.28841 1.85562,-4.143366 4.14402,-4.144027 z m -48.662414,6.184843 h 5.527101 v 5.527103 h 5.527103 v 5.527102 H 51.591319 V -32.886 h -5.527101 v -5.527102 h -5.527103 v -5.527102 h 5.527103 z m 33.10802,0 c 4.578848,-1.12e-4 8.290764,3.711806 8.290654,8.290654 1.1e-4,4.578848 -3.711806,8.290765 -8.290654,8.290653 -4.578848,1.13e-4 -8.290766,-3.711805 -8.290654,-8.290653 -1.13e-4,-4.578848 3.711806,-8.290767 8.290654,-8.290654 z"
- id="path3849"
- inkscape:connector-curvature="0"
- sodipodi:nodetypes="cccccccscscccccccccccccccccccccccccccccccccccccccccccccccccccccc" />
- </g>
-</svg>
diff --git a/src/stylesheets/Paper/Automata/Toolbar/nexus.svg b/src/stylesheets/Paper/Automata/Toolbar/nexus.svg
deleted file mode 100644
index 852f0034..00000000
--- a/src/stylesheets/Paper/Automata/Toolbar/nexus.svg
+++ /dev/null
@@ -1,122 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
- xmlns:dc="http://purl.org/dc/elements/1.1/"
- xmlns:cc="http://creativecommons.org/ns#"
- xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
- xmlns:svg="http://www.w3.org/2000/svg"
- xmlns="http://www.w3.org/2000/svg"
- xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
- xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
- width="128"
- height="128"
- viewBox="0 0 128 128"
- version="1.1"
- id="svg8"
- inkscape:export-filename="D:\Bethesda Game Modding\Mod Organizer 2\stylesheets\Paper\Dark\dots.png"
- inkscape:export-xdpi="307.20001"
- inkscape:export-ydpi="307.20001"
- inkscape:version="0.92.1 r15371"
- sodipodi:docname="nexus.svg">
- <defs
- id="defs2" />
- <sodipodi:namedview
- id="base"
- pagecolor="#505050"
- bordercolor="#666666"
- borderopacity="1.0"
- inkscape:pageopacity="0"
- inkscape:pageshadow="2"
- inkscape:zoom="12.991372"
- inkscape:cx="76.213845"
- inkscape:cy="37.493768"
- inkscape:document-units="px"
- inkscape:current-layer="layer25"
- showgrid="false"
- units="px"
- borderlayer="true"
- inkscape:showpageshadow="false"
- inkscape:snap-page="true"
- inkscape:snap-bbox="true"
- inkscape:bbox-nodes="true"
- inkscape:snap-bbox-edge-midpoints="true"
- inkscape:snap-bbox-midpoints="true"
- showguides="true"
- inkscape:guide-bbox="true"
- inkscape:window-width="1920"
- inkscape:window-height="1027"
- inkscape:window-x="-8"
- inkscape:window-y="-8"
- inkscape:window-maximized="1"
- inkscape:snap-center="true"
- inkscape:snap-object-midpoints="true"
- inkscape:measure-start="108.073,108.073"
- inkscape:measure-end="112.737,112.737"
- inkscape:snap-intersection-paths="true"
- inkscape:object-paths="true"
- inkscape:snap-smooth-nodes="true"
- inkscape:snap-midpoints="true"
- inkscape:snap-global="true">
- <sodipodi:guide
- position="64,64"
- orientation="1,0"
- id="guide4039"
- inkscape:locked="false" />
- <sodipodi:guide
- position="64,64"
- orientation="0,1"
- id="guide3935"
- inkscape:locked="false"
- inkscape:label="255"
- inkscape:color="rgb(0,0,255)" />
- </sodipodi:namedview>
- <metadata
- id="metadata5">
- <rdf:RDF>
- <cc:Work
- rdf:about="">
- <dc:format>image/svg+xml</dc:format>
- <dc:type
- rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
- <dc:title />
- </cc:Work>
- </rdf:RDF>
- </metadata>
- <g
- inkscape:groupmode="layer"
- id="layer25"
- inkscape:label="Content"
- transform="translate(0,108)">
- <path
- style="opacity:1;fill:#4e4b42;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:7.99999952;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:normal"
- d="m 37.578906,-103.99986 -2.327345,1.09219 c -5.625077,2.65309 -12.341703,6.959572 -18.124217,14.742184 l -1.439062,1.942967 -0.3,2.397657 c -0.727944,5.785703 0.645537,10.927203 2.601562,15.550782 -3.97001,7.523426 -6.539675,15.713366 -6.546095,24.274216 h 0.01176 v 0.06094 c 0.01254,2.097981 0.399663,4.072256 0.646875,6.100783 -3.5620623,4.427533 -7.0313403,9.956846 -7.8375002,17.704685 l -0.2648844,2.554689 1.0921859,2.327343 c 2.6531016,5.6250757 6.9595777,12.341692 14.7421887,18.12422 l 1.94297,1.4390639 2.397656,0.3 c 5.785701,0.727944 10.927203,-0.6455399 15.55078,-2.6015644 7.523428,3.9700084 15.713368,6.539664 24.274217,6.546096 v -0.012 h 0.06094 c 2.097982,-0.0126 4.072257,-0.39966 6.100783,-0.646872 4.427536,3.5620555 9.956847,7.0313275 17.704687,7.8374995 l 2.554687,0.26484 2.327343,-1.092192 C 98.373512,12.254575 105.09014,7.9480995 110.87265,0.1654875 l 1.43672,-1.9429715 0.30236,-2.3976556 c 0.72794,-5.7854267 -0.64555,-10.9269284 -2.60157,-15.5505044 3.97001,-7.523428 6.53967,-15.713368 6.5461,-24.27422 h -0.012 v -0.06094 c -0.0133,-2.097984 -0.39967,-4.072258 -0.64688,-6.100783 3.56206,-4.427535 7.03131,-9.956848 7.8375,-17.704688 L 124,-70.420959 l -1.09219,-2.327345 c -2.6531,-5.625076 -6.95957,-12.341702 -14.74219,-18.124218 l -1.94297,-1.436719 -2.39765,-0.302345 c -5.785706,-0.727943 -10.927203,0.64554 -15.550778,2.601564 -7.523434,-3.970005 -15.713374,-6.539667 -24.274225,-6.546088 v 0.01172 h -0.06094 c -2.097982,0.01266 -4.072255,0.399662 -6.100782,0.646874 -4.427534,-3.562058 -9.956847,-7.031344 -17.704687,-7.837494 z m 1.485937,6.351563 c 0.03984,-7.78e-4 0.07969,-7.78e-4 0.119532,0 0.08844,8.72e-4 0.176806,0.0056 0.264842,0.01406 0.01563,-1.23e-4 0.03128,-1.23e-4 0.04686,0 0.06038,0.0056 0.120558,0.01378 0.180468,0.02344 0.03369,0.0037 0.06016,0.01978 0.09376,0.02344 8.119439,0.924618 13.438311,4.913385 17.695312,8.847652 0.518092,0.475264 0.857314,1.113992 0.960938,1.809374 1.846847,-0.259602 3.708531,-0.399736 5.573432,-0.419528 7.414774,0.0056 14.704137,1.913317 21.171093,5.540624 3.592503,-2.496611 7.615515,-4.335787 12.384376,-4.79531 1.744997,-0.168149 3.586634,-0.159114 5.503124,0.082 0.5255,0.06779 1.0245,0.270451 1.44845,0.588283 6.86031,5.097232 10.33844,10.548846 12.76641,15.658591 0.0245,0.05156 0.0622,0.100836 0.0867,0.152346 0.0157,0.04031 0.0306,0.08093 0.0445,0.121879 0.0412,0.09035 0.0779,0.182649 0.11015,0.276564 0,0.0029 0,0.0063 0,0.0094 4e-5,0.0086 4e-5,0.01718 0,0.02577 0.0284,0.09024 0.0527,0.18174 0.0727,0.274217 0.0197,0.09143 0.0355,0.18371 0.0469,0.276565 1e-5,0.0079 1e-5,0.01562 0,0.02344 0.005,0.06 0.0108,0.120187 0.0141,0.180468 7.7e-4,0.03984 7.7e-4,0.07969 0,0.119532 -8.7e-4,0.08844 -0.005,0.176808 -0.0141,0.264844 1.2e-4,0.01563 1.2e-4,0.03125 0,0.04685 -0.008,0.06038 -0.0139,0.12056 -0.0233,0.180468 -0.004,0.03369 -0.0199,0.06016 -0.0234,0.09376 -0.92463,8.119438 -4.91339,13.438312 -8.84766,17.695313 -0.47526,0.51809 -1.114,0.857312 -1.80939,0.960935 0.25962,1.846853 0.39976,3.708535 0.41955,5.573439 -0.005,7.414773 -1.91333,14.704135 -5.54064,21.171093 2.49662,3.592503 4.33578,7.615515 4.79533,12.384375 0.16814,1.7449932 0.1591,3.586632 -0.0819,5.5031256 -0.0677,0.5254956 -0.27045,1.0244952 -0.58828,1.4484372 -5.09716,6.8603052 -10.548768,10.3384447 -15.658515,12.7664047 -0.05156,0.02448 -0.100836,0.06228 -0.152342,0.08676 -0.04031,0.01572 -0.08092,0.0306 -0.12188,0.04452 -0.09035,0.04116 -0.182648,0.07788 -0.276562,0.11016 -0.0031,0 -0.0063,0 -0.0094,0 -0.0086,3.5e-5 -0.01718,3.5e-5 -0.02577,0 -0.09024,0.02844 -0.181741,0.05268 -0.274216,0.07272 -0.09143,0.0198 -0.183711,0.03552 -0.276563,0.04692 -0.0079,2.4e-5 -0.01563,2.4e-5 -0.02344,0 -0.06,0.006 -0.120186,0.0108 -0.180468,0.01404 -0.03984,7.8e-4 -0.0797,7.8e-4 -0.119532,0 -0.08844,-8.64e-4 -0.176806,-0.006 -0.264846,-0.01404 -0.01563,1.2e-4 -0.03128,1.2e-4 -0.04686,0 -0.06037,-0.006 -0.120559,-0.0138 -0.180469,-0.0234 -0.03369,-0.0036 -0.06014,-0.0198 -0.09376,-0.0234 C 80.110654,8.6632245 74.79178,4.6744608 70.534779,0.740188 c -0.51809,-0.47526 -0.857313,-1.113984 -0.960936,-1.8093745 -1.847084,0.259404 -3.708765,0.3995425 -5.573668,0.4193305 -7.414776,-0.006 -14.704135,-1.9133172 -21.171094,-5.5406236 -3.592503,2.4966096 -7.615515,4.3357863 -12.384377,4.7953119 -1.744993,0.1681465 -3.586632,0.1591105 -5.503122,-0.082008 -0.525498,-0.067788 -1.024497,-0.2704523 -1.448438,-0.5882844 -6.860299,-5.0972295 -10.338443,-10.5488439 -12.766408,-15.6585929 -0.0245,-0.05156 -0.06234,-0.10086 -0.08672,-0.152343 -0.0157,-0.0403 -0.03056,-0.08095 -0.04453,-0.12188 -0.04117,-0.09035 -0.07795,-0.182646 -0.11016,-0.276563 -6e-6,-0.0031 -6e-6,-0.0063 0,-0.0094 -3.6e-5,-0.0086 -3.6e-5,-0.01718 0,-0.02577 -0.02843,-0.09024 -0.05268,-0.181742 -0.07265,-0.274218 -0.01984,-0.09143 -0.03547,-0.183709 -0.04685,-0.276562 -3.1e-5,-0.0077 -3.1e-5,-0.01562 0,-0.02344 -0.0067,-0.06 -0.01116,-0.120186 -0.0141,-0.180469 -7.76e-4,-0.03984 -7.76e-4,-0.07969 0,-0.119532 8.7e-4,-0.08844 0.0053,-0.176808 0.0141,-0.264847 -1.19e-4,-0.01563 -1.19e-4,-0.03125 0,-0.04685 0.006,-0.06037 0.0138,-0.120559 0.02344,-0.180469 0.0037,-0.03369 0.01981,-0.06014 0.02344,-0.09376 0.924627,-8.119438 4.91339,-13.438313 8.847658,-17.695312 0.475263,-0.518093 1.113993,-0.857315 1.809373,-0.960938 -0.259589,-1.846868 -0.399724,-3.708551 -0.419516,-5.573451 0.0056,-7.414775 1.913317,-14.704137 5.540624,-21.171094 -2.49661,-3.592502 -4.335787,-7.615515 -4.79531,-12.384377 -0.168149,-1.744994 -0.159113,-3.58663 0.082,-5.503124 0.06779,-0.525498 0.270452,-1.024496 0.588283,-1.448437 5.097232,-6.860299 10.548845,-10.338443 15.658591,-12.766403 0.05156,-0.02447 0.100837,-0.06234 0.152347,-0.08672 0.04031,-0.01575 0.08093,-0.03056 0.121878,-0.04453 0.09035,-0.04116 0.182646,-0.07791 0.276563,-0.110157 0.0029,0 0.0063,0 0.0094,0 0.0086,-3.7e-5 0.01718,-3.7e-5 0.02578,0 0.09024,-0.02841 0.181741,-0.05269 0.274218,-0.07266 0.09143,-0.01987 0.183711,-0.03544 0.276563,-0.04687 0.0077,-2.8e-5 0.01563,-2.8e-5 0.02344,0 0.06,-0.0066 0.120186,-0.01116 0.180468,-0.01416 z m 0.119532,3.070311 c -4.918893,2.320027 -9.830756,5.400922 -14.6625,11.903905 -0.106116,0.843338 -0.161184,1.665537 -0.171096,2.467968 -0.0098,0.802433 0.02693,1.584662 0.100776,2.350782 1.033521,10.725648 10.216452,17.923996 18.292968,25.258593 l -3.764063,4.246876 21.255469,-0.131254 -2.578125,-20.934377 -4.446092,5.025 c -7.369823,-8.23868 -18.074776,-20.906037 -14.02734,-30.187495 z m 3.009373,0.623437 c -0.04253,0.205977 -0.106176,0.407033 -0.189841,0.599999 -0.355764,0.815838 -0.552587,1.702983 -0.602346,2.660157 -0.01586,0.305199 -0.01586,0.61855 0,0.9375 0.111036,2.232626 0.98277,4.789133 2.3625,7.502343 3.563011,-1.890229 7.372419,-3.27389 11.317968,-4.110937 l 0.3,-0.119533 c -3.54753,-3.278554 -7.503737,-6.172443 -13.188279,-7.46953 z m 12.2625,4.582031 -0.215624,0.07031 -0.01176,0.02344 c 0.08105,-0.01726 0.146146,-0.07651 0.227344,-0.09376 z m 45.750002,5.723437 c -0.802434,-0.0098 -1.584666,0.02696 -2.350781,0.100775 -10.725652,1.03352 -17.923998,10.216453 -25.258595,18.292968 l -4.246878,-3.764057 0.131254,21.255469 20.934376,-2.578123 -5.025,-4.446096 c 8.238683,-7.369823 20.906044,-18.072433 30.187494,-14.025 -2.32001,-4.918892 -5.40091,-9.833098 -11.9039,-14.664842 -0.84333,-0.106117 -1.66554,-0.161182 -2.46797,-0.171094 z m 9.55079,17.050783 c -2.23265,0.111035 -4.78915,0.982766 -7.50235,2.362499 1.89023,3.563012 3.27389,7.372418 4.11093,11.317966 l 0.11965,0.3 c 3.27856,-3.547531 6.17244,-7.503737 7.46954,-13.18828 -0.20598,-0.04252 -0.40703,-0.106175 -0.6,-0.189842 -0.81584,-0.355764 -1.70298,-0.552585 -2.66016,-0.602343 -0.30519,-0.01586 -0.61855,-0.01586 -0.9375,0 z m -0.47813,12.827343 c 0.0173,0.08105 0.0764,0.146144 0.0937,0.227342 l -0.0702,-0.215625 z m -49.762503,13.535156 -20.932032,2.578124 5.025,4.446095 c -8.238684,7.369824 -20.906042,18.074776 -30.1875,14.027343 2.320026,4.918894 5.400921,9.8307519 11.903905,14.662499 0.843337,0.106116 1.665539,0.1611829 2.467968,0.1710949 0.802431,0.00984 1.584663,-0.026952 2.350782,-0.100776 10.72565,-1.0335217 17.923997,-10.2164539 25.258593,-18.2929699 l 4.246874,3.764056 z m 29.503121,0.585935 -21.255467,0.133592 2.578124,20.932032 4.446095,-5.025 c 7.369824,8.238685 18.072432,20.9060398 14.024999,30.1875015 4.91889,-2.3200323 9.833102,-5.4009245 14.664841,-11.9039095 0.10608,-0.8433371 0.16119,-1.6655363 0.1711,-2.4679691 0.01,-0.8024304 -0.027,-1.5846612 -0.1008,-2.3507799 C 102.5149,-20.870044 93.331956,-28.06839 85.255441,-35.402988 Z m -67.504684,4.267971 c -3.278554,3.547531 -6.172444,7.503737 -7.469533,13.18828 0.205978,0.04256 0.407034,0.106176 0.6,0.189843 0.815838,0.355762 1.702981,0.552586 2.660158,0.602343 0.305198,0.01586 0.618549,0.01586 0.9375,0 2.232626,-0.111036 4.789133,-0.982768 7.502342,-2.3625 -1.890227,-3.56301 -3.273887,-7.372418 -4.110936,-11.317966 z m -2.8875,0.92578 0.0703,0.215625 0.02344,0.01176 c -0.01726,-0.08105 -0.0765,-0.146143 -0.09375,-0.227344 z m 65.608592,28.7109367 c -3.563012,1.8902291 -7.37242,3.2738893 -11.317967,4.1109373 l -0.3,0.119532 c 3.54753,3.2785476 7.503737,6.1724429 13.188278,7.4695349 0.04252,-0.20598 0.106177,-0.407028 0.189844,-0.6 0.355763,-0.815844 0.552585,-1.7029799 0.602345,-2.6601593 0.01586,-0.305196 0.01586,-0.618552 0,-0.9375 -0.111036,-2.2326384 -0.982768,-4.789134 -2.3625,-7.5023449 z M 73.771091,1.2790394 c -0.08105,0.01728 -0.146145,0.07644 -0.227344,0.09372 l 0.215627,-0.07032 z"
- id="path3849"
- inkscape:connector-curvature="0"
- sodipodi:nodetypes="cccccccccccccccccccccccccccccccccccccccccccccccccccccccscccccccccccccccccccccscccccccccccccccccccccscccccccccccccccccccccscccccccccccccccccccccccccsccccccccccccccccccccccccccccccccccccccccccccccccccccccsccccccccccccccccccc" />
- <g
- style="fill:#505050;fill-opacity:1;stroke-width:0.19550784"
- id="g4412"
- inkscape:transform-center-x="-5.0913502"
- inkscape:transform-center-y="-4.4596393"
- transform="matrix(5.1148849,0,0,5.1148849,12.851148,-95.148848)" />
- <g
- transform="matrix(0,4.9102895,-4.9102895,0,113.1029,-93.102895)"
- inkscape:transform-center-y="24"
- inkscape:transform-center-x="-21.022466"
- id="g4064"
- style="fill:#505050;fill-opacity:1;stroke-width:0.20365398" />
- <g
- style="fill:#505050;fill-opacity:1;stroke-width:0.20365398"
- id="g4068"
- inkscape:transform-center-x="24.000005"
- inkscape:transform-center-y="21.022466"
- transform="matrix(-4.9102895,0,0,-4.9102895,113.10289,5.1029002)" />
- <g
- transform="matrix(-2.719147e-8,-4.9102894,4.9102894,-2.719147e-8,14.8971,5.1028901)"
- inkscape:transform-center-y="-24.000003"
- inkscape:transform-center-x="21.022466"
- id="g4072"
- style="fill:#505050;fill-opacity:1;stroke-width:0.20365398" />
- </g>
-</svg>
diff --git a/src/stylesheets/Paper/Automata/Toolbar/problems.svg b/src/stylesheets/Paper/Automata/Toolbar/problems.svg
deleted file mode 100644
index f3fed97c..00000000
--- a/src/stylesheets/Paper/Automata/Toolbar/problems.svg
+++ /dev/null
@@ -1,132 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
- xmlns:dc="http://purl.org/dc/elements/1.1/"
- xmlns:cc="http://creativecommons.org/ns#"
- xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
- xmlns:svg="http://www.w3.org/2000/svg"
- xmlns="http://www.w3.org/2000/svg"
- xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
- xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
- width="128"
- height="128"
- viewBox="0 0 128 128"
- version="1.1"
- id="svg8"
- inkscape:export-filename="D:\Bethesda Game Modding\Mod Organizer 2\stylesheets\Paper\Dark\dots.png"
- inkscape:export-xdpi="307.20001"
- inkscape:export-ydpi="307.20001"
- inkscape:version="0.92.1 r15371"
- sodipodi:docname="problems.svg">
- <defs
- id="defs2" />
- <sodipodi:namedview
- id="base"
- pagecolor="#505050"
- bordercolor="#666666"
- borderopacity="1.0"
- inkscape:pageopacity="0"
- inkscape:pageshadow="2"
- inkscape:zoom="10.201449"
- inkscape:cx="14.047399"
- inkscape:cy="24.756816"
- inkscape:document-units="px"
- inkscape:current-layer="layer25"
- showgrid="false"
- units="px"
- borderlayer="true"
- inkscape:showpageshadow="false"
- inkscape:snap-page="true"
- inkscape:snap-bbox="true"
- inkscape:bbox-nodes="true"
- inkscape:snap-bbox-edge-midpoints="true"
- inkscape:snap-bbox-midpoints="true"
- showguides="false"
- inkscape:guide-bbox="true"
- inkscape:window-width="1920"
- inkscape:window-height="1027"
- inkscape:window-x="-8"
- inkscape:window-y="-8"
- inkscape:window-maximized="1"
- inkscape:snap-center="true"
- inkscape:snap-object-midpoints="true"
- inkscape:measure-start="18.8975,12.498"
- inkscape:measure-end="20,12.498"
- inkscape:snap-intersection-paths="true"
- inkscape:object-paths="true"
- inkscape:snap-smooth-nodes="true"
- inkscape:snap-midpoints="true"
- inkscape:snap-global="true">
- <sodipodi:guide
- position="64,64"
- orientation="1,0"
- id="guide4039"
- inkscape:locked="false" />
- <sodipodi:guide
- position="64,64"
- orientation="0,1"
- id="guide3935"
- inkscape:locked="false"
- inkscape:label=""
- inkscape:color="rgb(0,0,255)" />
- </sodipodi:namedview>
- <metadata
- id="metadata5">
- <rdf:RDF>
- <cc:Work
- rdf:about="">
- <dc:format>image/svg+xml</dc:format>
- <dc:type
- rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
- <dc:title />
- </cc:Work>
- </rdf:RDF>
- </metadata>
- <g
- inkscape:groupmode="layer"
- id="layer25"
- inkscape:label="Content"
- transform="translate(0,108)">
- <g
- id="g3861"
- transform="matrix(0.55290621,0,0,0.55290621,28.614002,-29.750801)"
- style="fill:#737373;fill-opacity:1;stroke-width:1.80862486" />
- <path
- style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:#4e4b42;stroke-width:7.99999952;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
- d="m 64.344931,-96.263216 c -2.808173,-0.131523 -5.455193,1.310482 -6.861125,3.737706 L 4.9422487,-1.7926865 C 2.0494853,3.2027814 5.6651359,9.4471972 11.450616,9.4476002 H 116.53373 c 5.78548,-4.1e-4 9.40112,-6.2448225 6.50836,-11.2402867 L 70.500541,-92.52551 c -1.279824,-2.209515 -3.599319,-3.61792 -6.15561,-3.737706 z"
- id="path3875"
- inkscape:connector-curvature="0"
- sodipodi:nodetypes="cccccccc" />
- <g
- transform="matrix(-0.83333333,0,0,-0.83333333,117.3255,-70.7681)"
- id="g3871"
- style="stroke-width:1.20000005;fill:#4e4b42;fill-opacity:1">
- <path
- inkscape:connector-curvature="0"
- id="path3851"
- d="m 64,-80 c -4.113298,1.67e-4 -7.447744,3.334612 -7.44791,7.44791 -6.7e-4,4.11389 3.33402,7.449258 7.44791,7.449426 4.11389,-1.68e-4 7.44858,-3.335536 7.44791,-7.449426 C 71.447743,-76.665387 68.113298,-79.999833 64,-80 Z"
- style="opacity:1;fill:#4e4b42;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:9.60000038;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:stroke fill markers" />
- <path
- inkscape:connector-curvature="0"
- id="path3860-8"
- d="m 56.55209,-50.206086 v 34.757418 h 14.89582 v -34.757418 z"
- style="opacity:1;fill:#4e4b42;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:9.60000038;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:stroke fill markers" />
- <path
- inkscape:connector-curvature="0"
- style="opacity:1;fill:#4e4b42;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:9.60000038;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:stroke fill markers"
- d="m 64,-80 c -4.113298,1.67e-4 -7.447744,3.334612 -7.44791,7.44791 -6.7e-4,4.11389 3.33402,7.449258 7.44791,7.449426 4.11389,-1.68e-4 7.44858,-3.335536 7.44791,-7.449426 C 71.447743,-76.665387 68.113298,-79.999833 64,-80 Z"
- id="path3853" />
- <path
- inkscape:connector-curvature="0"
- id="path3862"
- d="m 64,-57.654754 c -4.113298,1.67e-4 -7.447744,3.334612 -7.44791,7.44791 -6.7e-4,4.11389 3.33402,7.449258 7.44791,7.449426 4.11389,-1.68e-4 7.44858,-3.335536 7.44791,-7.449426 -1.67e-4,-4.113297 -3.334612,-7.447743 -7.44791,-7.44791 z"
- style="opacity:1;fill:#4e4b42;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:9.60000038;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:stroke fill markers" />
- <path
- style="opacity:1;fill:#4e4b42;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:9.60000038;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:stroke fill markers"
- d="m 64,-22.897336 c -4.113298,1.67e-4 -7.447744,3.334612 -7.44791,7.44791 C 56.55142,-11.335536 59.88611,-8.000168 64,-8 c 4.11389,-1.68e-4 7.44858,-3.335536 7.44791,-7.449426 -1.67e-4,-4.113297 -3.334612,-7.447743 -7.44791,-7.44791 z"
- id="path3864"
- inkscape:connector-curvature="0" />
- </g>
- </g>
-</svg>
diff --git a/src/stylesheets/Paper/Automata/Toolbar/profiles.svg b/src/stylesheets/Paper/Automata/Toolbar/profiles.svg
deleted file mode 100644
index 306c94f5..00000000
--- a/src/stylesheets/Paper/Automata/Toolbar/profiles.svg
+++ /dev/null
@@ -1,112 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
- xmlns:dc="http://purl.org/dc/elements/1.1/"
- xmlns:cc="http://creativecommons.org/ns#"
- xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
- xmlns:svg="http://www.w3.org/2000/svg"
- xmlns="http://www.w3.org/2000/svg"
- xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
- xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
- width="128"
- height="128"
- viewBox="0 0 128 128"
- version="1.1"
- id="svg8"
- inkscape:export-filename="D:\Bethesda Game Modding\Mod Organizer 2\stylesheets\Paper\Dark\dots.png"
- inkscape:export-xdpi="307.20001"
- inkscape:export-ydpi="307.20001"
- inkscape:version="0.92.1 r15371"
- sodipodi:docname="profiles.svg">
- <defs
- id="defs2" />
- <sodipodi:namedview
- id="base"
- pagecolor="#505050"
- bordercolor="#666666"
- borderopacity="1.0"
- inkscape:pageopacity="0"
- inkscape:pageshadow="2"
- inkscape:zoom="14.003331"
- inkscape:cx="54.048348"
- inkscape:cy="44.931547"
- inkscape:document-units="px"
- inkscape:current-layer="layer25"
- showgrid="false"
- units="px"
- borderlayer="true"
- inkscape:showpageshadow="false"
- inkscape:snap-page="true"
- inkscape:snap-bbox="true"
- inkscape:bbox-nodes="true"
- inkscape:snap-bbox-edge-midpoints="true"
- inkscape:snap-bbox-midpoints="true"
- showguides="true"
- inkscape:guide-bbox="true"
- inkscape:window-width="1920"
- inkscape:window-height="1027"
- inkscape:window-x="-8"
- inkscape:window-y="-8"
- inkscape:window-maximized="1"
- inkscape:snap-center="true"
- inkscape:snap-object-midpoints="true"
- inkscape:measure-start="64,11.9981"
- inkscape:measure-end="64,7.93514"
- inkscape:snap-intersection-paths="true"
- inkscape:object-paths="true"
- inkscape:snap-smooth-nodes="true"
- inkscape:snap-midpoints="true"
- inkscape:snap-global="true"
- inkscape:lockguides="true"
- inkscape:snap-nodes="false">
- <sodipodi:guide
- position="64,64"
- orientation="1,0"
- id="guide4039"
- inkscape:locked="true" />
- <sodipodi:guide
- position="64,64"
- orientation="0,1"
- id="guide3935"
- inkscape:locked="true"
- inkscape:label=""
- inkscape:color="rgb(0,0,255)" />
- </sodipodi:namedview>
- <metadata
- id="metadata5">
- <rdf:RDF>
- <cc:Work
- rdf:about="">
- <dc:format>image/svg+xml</dc:format>
- <dc:type
- rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
- <dc:title />
- </cc:Work>
- </rdf:RDF>
- </metadata>
- <g
- inkscape:groupmode="layer"
- id="layer25"
- inkscape:label="Content"
- transform="translate(0,108)">
- <g
- id="g3856"
- style="fill:#4e4b42;fill-opacity:1;stroke-width:0.81111115"
- transform="matrix(1.2328767,0,0,1.2328767,-14.376763,-8.863014)">
- <circle
- r="21"
- cy="-44"
- cx="64"
- id="path3874"
- style="opacity:1;fill:#4e4b42;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:3.24444389;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:stroke fill markers" />
- <path
- sodipodi:nodetypes="ccsscc"
- inkscape:connector-curvature="0"
- id="path3858"
- transform="translate(0,-108)"
- d="M 63.421875,89.005859 C 48.016021,89.029098 33.691706,97.71624 25.455078,112.03125 29.673503,114.55644 34.640748,116 40,116 h 48 c 5.001681,0 9.660535,-1.26009 13.68945,-3.48047 C 93.502774,97.918077 79.022242,89.020453 63.421875,89.005859 Z"
- style="opacity:1;fill:#4e4b42;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.40555558;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:normal" />
- </g>
- </g>
-</svg>
diff --git a/src/stylesheets/Paper/Automata/Toolbar/settings.svg b/src/stylesheets/Paper/Automata/Toolbar/settings.svg
deleted file mode 100644
index adc4ca7a..00000000
--- a/src/stylesheets/Paper/Automata/Toolbar/settings.svg
+++ /dev/null
@@ -1,95 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
- xmlns:dc="http://purl.org/dc/elements/1.1/"
- xmlns:cc="http://creativecommons.org/ns#"
- xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
- xmlns:svg="http://www.w3.org/2000/svg"
- xmlns="http://www.w3.org/2000/svg"
- xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
- xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
- width="128"
- height="128"
- viewBox="0 0 128 128"
- version="1.1"
- id="svg8"
- inkscape:export-filename="D:\Bethesda Game Modding\Mod Organizer 2\stylesheets\Paper\Dark\dots.png"
- inkscape:export-xdpi="307.20001"
- inkscape:export-ydpi="307.20001"
- inkscape:version="0.92.1 r15371"
- sodipodi:docname="settings.svg">
- <defs
- id="defs2" />
- <sodipodi:namedview
- id="base"
- pagecolor="#505050"
- bordercolor="#666666"
- borderopacity="1.0"
- inkscape:pageopacity="0"
- inkscape:pageshadow="2"
- inkscape:zoom="17.011826"
- inkscape:cx="26.380278"
- inkscape:cy="24.43076"
- inkscape:document-units="px"
- inkscape:current-layer="layer25"
- showgrid="false"
- units="px"
- borderlayer="true"
- inkscape:showpageshadow="false"
- inkscape:snap-page="true"
- inkscape:snap-bbox="true"
- inkscape:bbox-nodes="true"
- inkscape:snap-bbox-edge-midpoints="true"
- inkscape:snap-bbox-midpoints="true"
- showguides="true"
- inkscape:guide-bbox="true"
- inkscape:window-width="1920"
- inkscape:window-height="1027"
- inkscape:window-x="-8"
- inkscape:window-y="-8"
- inkscape:window-maximized="1"
- inkscape:snap-center="true"
- inkscape:snap-object-midpoints="true"
- inkscape:measure-start="8,64"
- inkscape:measure-end="17.9742,64"
- inkscape:snap-intersection-paths="true"
- inkscape:object-paths="true"
- inkscape:snap-smooth-nodes="true"
- inkscape:snap-midpoints="true"
- inkscape:snap-global="true">
- <sodipodi:guide
- position="64,64"
- orientation="1,0"
- id="guide4039"
- inkscape:locked="false" />
- <sodipodi:guide
- position="64,64"
- orientation="0,1"
- id="guide3854"
- inkscape:locked="false" />
- </sodipodi:namedview>
- <metadata
- id="metadata5">
- <rdf:RDF>
- <cc:Work
- rdf:about="">
- <dc:format>image/svg+xml</dc:format>
- <dc:type
- rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
- <dc:title />
- </cc:Work>
- </rdf:RDF>
- </metadata>
- <g
- inkscape:groupmode="layer"
- id="layer25"
- inkscape:label="Content"
- transform="translate(0,108)">
- <path
- style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#4e4b42;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:6;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
- d="m 55.75,-99 v 4.125 7.809571 c -5.874077,1.121199 -11.426558,3.415826 -16.37915,6.767577 l -8.427247,-8.427246 -11.668701,11.668701 8.429933,8.429932 c -3.36362,4.949109 -5.670511,10.500514 -6.802492,16.376465 H 9 v 16.5 h 11.934571 c 1.121199,5.874077 3.415826,11.426558 6.767577,16.37915 l -8.427246,8.427247 11.668701,11.66870105 8.429933,-8.42993305 c 4.949109,3.36362 10.500513,5.6705116 16.376464,6.80249075 V 11 h 16.5 V -0.93457095 C 78.124077,-2.0557723 83.676558,-4.3503989 88.62915,-7.702148 l 8.42725,8.42724605 11.6687,-11.66870105 -8.42994,-8.429933 c 3.36362,-4.949109 5.67052,-10.500513 6.8025,-16.376464 H 119 v -16.5 h -11.93457 c -1.1212,-5.874077 -3.41582,-11.426558 -6.76757,-16.37915 l 8.42724,-8.427247 -11.6687,-11.668701 -8.429935,8.429932 C 83.677355,-83.658785 78.125951,-85.965676 72.25,-87.097656 V -99 Z M 64,-68.75 c 13.603902,0 24.75,11.146098 24.75,24.75 0,13.603902 -11.146098,24.75 -24.75,24.75 -13.603902,0 -24.75,-11.146098 -24.75,-24.75 0,-13.603902 11.146098,-24.75 24.75,-24.75 z m 0,11 c -0.478691,0 -0.949948,0.02335 -1.415283,0.06982 -6.488691,0.656052 -11.608841,5.776202 -12.264893,12.264893 -1.79e-4,0.0018 1.79e-4,0.0036 0,0.0054 -0.04613,0.463616 -0.06983,0.933021 -0.06983,1.4099 0,0.476656 0.02311,0.946126 0.06982,1.409913 1.76e-4,0.0018 -1.76e-4,0.0036 0,0.0054 0.656052,6.488691 5.776202,11.60884 12.264893,12.264892 0.0018,1.79e-4 0.0035,-1.78e-4 0.0054,0 0.463616,0.04611 0.933021,0.06981 1.4099,0.06981 0.476878,0 0.946282,-0.02369 1.409913,-0.06982 0.0018,-1.78e-4 0.0035,1.78e-4 0.0054,0 6.488691,-0.656052 11.60884,-5.776202 12.264892,-12.264893 0.04705,-0.465516 0.06981,-0.936808 0.06981,-1.41529 0,-0.478482 -0.02276,-0.949774 -0.06982,-1.415283 -0.656052,-6.488691 -5.776203,-11.608841 -12.264894,-12.264893 -0.0018,-1.76e-4 -0.0035,1.76e-4 -0.0054,0 -0.463773,-0.04671 -0.933243,-0.06983 -1.409899,-0.06983 z"
- id="path3915"
- inkscape:connector-curvature="0" />
- </g>
-</svg>
diff --git a/src/stylesheets/Paper/Automata/Toolbar/tools.svg b/src/stylesheets/Paper/Automata/Toolbar/tools.svg
deleted file mode 100644
index 555472e8..00000000
--- a/src/stylesheets/Paper/Automata/Toolbar/tools.svg
+++ /dev/null
@@ -1,117 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
- xmlns:dc="http://purl.org/dc/elements/1.1/"
- xmlns:cc="http://creativecommons.org/ns#"
- xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
- xmlns:svg="http://www.w3.org/2000/svg"
- xmlns="http://www.w3.org/2000/svg"
- xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
- xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
- width="128"
- height="128"
- viewBox="0 0 128 128"
- version="1.1"
- id="svg8"
- inkscape:export-filename="D:\Bethesda Game Modding\Mod Organizer 2\stylesheets\Paper\Dark\dots.png"
- inkscape:export-xdpi="307.20001"
- inkscape:export-ydpi="307.20001"
- inkscape:version="0.92.1 r15371"
- sodipodi:docname="tools.svg">
- <defs
- id="defs2" />
- <sodipodi:namedview
- id="base"
- pagecolor="#505050"
- bordercolor="#666666"
- borderopacity="1.0"
- inkscape:pageopacity="0"
- inkscape:pageshadow="2"
- inkscape:zoom="9.90185"
- inkscape:cx="62.575038"
- inkscape:cy="1.523437"
- inkscape:document-units="px"
- inkscape:current-layer="layer25"
- showgrid="false"
- units="px"
- borderlayer="true"
- inkscape:showpageshadow="false"
- inkscape:snap-page="true"
- inkscape:snap-bbox="true"
- inkscape:bbox-nodes="true"
- inkscape:snap-bbox-edge-midpoints="true"
- inkscape:snap-bbox-midpoints="true"
- showguides="true"
- inkscape:guide-bbox="true"
- inkscape:window-width="1920"
- inkscape:window-height="1027"
- inkscape:window-x="-8"
- inkscape:window-y="-8"
- inkscape:window-maximized="1"
- inkscape:snap-center="true"
- inkscape:snap-object-midpoints="true"
- inkscape:measure-start="54,64"
- inkscape:measure-end="64,64"
- inkscape:snap-intersection-paths="true"
- inkscape:object-paths="true"
- inkscape:snap-smooth-nodes="true"
- inkscape:snap-midpoints="true"
- inkscape:snap-global="true"
- inkscape:bbox-paths="true">
- <sodipodi:guide
- position="64,64"
- orientation="1,0"
- id="guide4039"
- inkscape:locked="false" />
- <sodipodi:guide
- position="64,64"
- orientation="0,1"
- id="guide3935"
- inkscape:locked="false"
- inkscape:label=""
- inkscape:color="rgb(0,0,255)" />
- </sodipodi:namedview>
- <metadata
- id="metadata5">
- <rdf:RDF>
- <cc:Work
- rdf:about="">
- <dc:format>image/svg+xml</dc:format>
- <dc:type
- rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
- <dc:title />
- </cc:Work>
- </rdf:RDF>
- </metadata>
- <g
- inkscape:groupmode="layer"
- id="layer25"
- inkscape:label="Content"
- transform="translate(0,108)">
- <g
- id="g3997"
- transform="matrix(0.55904332,-0.55901902,-0.55904332,-0.55901902,4.4028105,-32.039863)"
- style="fill:#4e4b42;fill-opacity:1;stroke-width:1.26487899">
- <path
- id="path3965"
- transform="translate(0,-108)"
- d="M -8,52 A 12,12 0 0 0 -20,64 12,12 0 0 0 -8,76 H 44 V 52 Z"
- style="opacity:1;fill:#4e4b42;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:5.05951595;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:stroke fill markers"
- inkscape:connector-curvature="0" />
- <path
- id="rect3969"
- transform="translate(0,-108)"
- d="m 128,53 -18,5 H 84 v 12 h 26 l 18,5 18,-5 V 58 Z"
- style="opacity:1;fill:#4e4b42;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:5.05951595;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:stroke fill markers"
- inkscape:connector-curvature="0" />
- <path
- style="opacity:1;fill:#4e4b42;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2.86491537;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:stroke fill markers"
- d="m 32.291016,50.410156 c -5.753358,0.0068 -10.878972,3.636181 -12.796875,9.060547 h 12.796875 c 2.50182,-5.4e-5 4.529351,2.027478 4.529296,4.529297 -5.4e-5,2.501819 -2.027476,4.531196 -4.529296,4.53125 H 19.496094 c 1.917396,5.423518 7.042374,9.051114 12.794922,9.058594 5.30786,-0.0064 10.126181,-3.102522 12.33789,-7.927735 H 64 105.23759 v -0.0039 c 0.0293,0.0015 0.0586,0.0028 0.0879,0.0039 3.12696,-3.3e-4 5.66178,-2.535151 5.6621,-5.662109 -3.2e-4,-3.126962 -2.53514,-5.661785 -5.66211,-5.662109 -0.0299,0.0011 -0.0599,0.0024 -0.0898,0.0039 v -0.0039 H 64 44.626953 c -2.211168,-4.82439 -7.028882,-7.920713 -12.335937,-7.927735 z"
- transform="matrix(-3.8373246e-5,-1.7660262,-1.7660262,-3.8373246e-5,177.02814,69.028134)"
- id="rect3945"
- inkscape:connector-curvature="0"
- sodipodi:nodetypes="cccsccccccccccccccc" />
- </g>
- </g>
-</svg>
diff --git a/src/stylesheets/Paper/Automata/Toolbar/update.svg b/src/stylesheets/Paper/Automata/Toolbar/update.svg
deleted file mode 100644
index 28bef275..00000000
--- a/src/stylesheets/Paper/Automata/Toolbar/update.svg
+++ /dev/null
@@ -1,141 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
- xmlns:dc="http://purl.org/dc/elements/1.1/"
- xmlns:cc="http://creativecommons.org/ns#"
- xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
- xmlns:svg="http://www.w3.org/2000/svg"
- xmlns="http://www.w3.org/2000/svg"
- xmlns:xlink="http://www.w3.org/1999/xlink"
- xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
- xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
- width="128"
- height="128"
- viewBox="0 0 128 128"
- version="1.1"
- id="svg8"
- inkscape:export-filename="D:\Bethesda Game Modding\Mod Organizer 2\stylesheets\Paper\Dark\dots.png"
- inkscape:export-xdpi="307.20001"
- inkscape:export-ydpi="307.20001"
- inkscape:version="0.92.1 r15371"
- sodipodi:docname="update.svg">
- <defs
- id="defs2">
- <clipPath
- clipPathUnits="userSpaceOnUse"
- id="clipPath4152-4-4-9">
- <rect
- style="fill:#fdf6e3;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:3;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
- id="rect4154-6-3-0"
- width="598.57141"
- height="928.57141"
- x="352.85715"
- y="22.362183" />
- </clipPath>
- </defs>
- <sodipodi:namedview
- id="base"
- pagecolor="#505050"
- bordercolor="#666666"
- borderopacity="1.0"
- inkscape:pageopacity="0"
- inkscape:pageshadow="2"
- inkscape:zoom="7.2039069"
- inkscape:cx="42.76634"
- inkscape:cy="57.923463"
- inkscape:document-units="px"
- inkscape:current-layer="layer25"
- showgrid="false"
- units="px"
- borderlayer="true"
- inkscape:showpageshadow="false"
- inkscape:snap-page="true"
- inkscape:snap-bbox="true"
- inkscape:bbox-nodes="true"
- inkscape:snap-bbox-edge-midpoints="true"
- inkscape:snap-bbox-midpoints="true"
- showguides="true"
- inkscape:guide-bbox="true"
- inkscape:window-width="1920"
- inkscape:window-height="1027"
- inkscape:window-x="-8"
- inkscape:window-y="-8"
- inkscape:window-maximized="1"
- inkscape:snap-center="true"
- inkscape:snap-object-midpoints="true"
- inkscape:measure-start="18.8975,12.498"
- inkscape:measure-end="20,12.498"
- inkscape:snap-intersection-paths="true"
- inkscape:object-paths="true"
- inkscape:snap-smooth-nodes="true"
- inkscape:snap-midpoints="true"
- inkscape:snap-global="true">
- <sodipodi:guide
- position="64,64"
- orientation="1,0"
- id="guide4039"
- inkscape:locked="false" />
- <sodipodi:guide
- position="64,64"
- orientation="0,1"
- id="guide3935"
- inkscape:locked="false"
- inkscape:label=""
- inkscape:color="rgb(0,0,255)" />
- </sodipodi:namedview>
- <metadata
- id="metadata5">
- <rdf:RDF>
- <cc:Work
- rdf:about="">
- <dc:format>image/svg+xml</dc:format>
- <dc:type
- rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
- <dc:title />
- </cc:Work>
- </rdf:RDF>
- </metadata>
- <g
- inkscape:groupmode="layer"
- id="layer25"
- inkscape:label="Content"
- transform="translate(0,108)">
- <g
- id="g4217-3-45"
- transform="matrix(0.50121193,0,0,0.50121193,-6.8260092,-112.83314)"
- style="display:inline;fill:#4e4b42;fill-opacity:1"
- inkscape:export-xdpi="96"
- inkscape:export-ydpi="96">
- <g
- style="display:none;fill:#4e4b42;fill-opacity:1"
- transform="translate(0,-783.36216)"
- id="layer1-9-66"
- inkscape:label="Reference">
- <image
- width="269"
- height="269"
- preserveAspectRatio="none"
- xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQ0AAAENCAYAAAAVEjAIAAAABHNCSVQICAgIfAhkiAAAIABJREFU eJzsnXd4HNXVh987s129WZJ7t+VuA8amGNOLA6GXQEIgQAotBAghJAQSEgKEJAQSekvoYHpJ4ANT jcEN27hXucjqfbVlduZ+f8zMatVsrSRbtjzv84iyO3PnzuzMb84599xzxaJSKXFwcHDoJEpvd8DB wWH/whENBweHpHBEw8HBISkc0XBwcEgKRzQcHBySwhENBweHpHBEw8HBISkc0XBwcEgKRzQcHByS whENBweHpHBEw8HBISkc0XBwcEgKRzQcHBySwhENBweHpHBEw8HBISkc0XBwcEgKRzQcHBySwhEN BweHpHBEw8HBISkc0XBwcEgKRzQcHBySwhENBweHpHBEw8HBISkc0XBwcEgKRzQcHBySwhENBweH pHBEw8HBISkc0XBwcEgKRzQcHBySwhENBweHpHBEw8HBISkc0XBwcEgKRzQcHBySwhENBweHpHBE w8HBISkc0XBwcEgKRzQcHBySwhENBweHpHBEw8HBISkc0XBwcEgKRzQcHBySwhENBweHpHBEw8HB ISkc0XBwcEgKRzQcHBySwhENBweHpHBEw8HBISkc0XBwcEgKRzQcHBySwhENBweHpHBEw8HBISkc 0XBwcEgKRzQcHBySwrW3DqRrEUqKN7J9yyaKN6xhS/FmYtEoUsq91YV9EsXlwuf1kZaaQkZGJjn9 CkjLLSQ3L4/C/gMJZOT0dhf3eaSh01hdxs6SEioryglWl1JdXUVDfT2NwSZC4TCGrmPoMRSXC5fb g9vtJjMtlfSMDLKycwhkF5Kfn09BYSGBzH69fUr7NHtMNKLBWjZv2sjObVvYsGo5WzZvpL6mimBj A5FQiFhMwzAMOMBFAwAhEEKgqioutwev14cvECCQkkpmVi6jxoxh4PAxDBk+kvyBQ1Fcnt7uca8S bqhha/Fmdm5Zz+YNa9m8aSPB+jpCTUFCoSa0SIRYTEPX9V3eX0JRUBQFt9uD2+vF5w8QCKSQmpnF 6NFjGDxiDP2HjmTg4GG4fSl78Qz3bcSi0p59andsXse7r77A1o1rqamupKmxgVBTkHCoiUg4jBaN oMdiGNIwRcMBACEEiqIghILL5cLlduNyufF4fXj9fnz+AGnpGaRnZjNi9BhGjp1E4eChDBo8BHcg o7e7v0dpqC5n86aNlBavZ+3K5ezYVkxDXS3BxgbCoSaikTBRWyhisU7fX8ISa0UoqAnX3O314vOZ 1zyQlk56eiZDh49g9PjJ5A8axrARo/Gl9u1rvit6TDRkLMr8j9/nvbnPUbJtC3U11YSaGlv+mLqO YehIKU23xLEymhHC+lfzjSwUBVVVUV0u3G4PLrcHnyUgPn+AQGoaaemZDBwylKLxExk4ooiho4rw +AK9fDLdo6a8hM3rV7N94xpWr1pJTVUF9bU1BBNeQNFIGC0aRddj8XtLGgYSmdz9ZV3vxGuuKJaI uNx4vF48Xh8+fwB/IIVAahrpGVnk5RcybsIE+g8by4gx40jJyN7zF2YfoUdEo76mijeeeZgFn82j rGQ79XU1hENNxDQNw9DjbogdvzjQ4xi7Q1gCYv+3UBTLElHjIuJyuXF7PLg93vgNnZqeQU5ePmOK JjDxoBmMHDuOQGZeL55J5zBiGts2rWXdiiV8u3wZpTuKqa2uosmyJCKRcFwk7BeQoevN1kTCvQVd u79aX3Msy08RCkqCcLs9HtP68/rwBVJITUsnIyuHQcNGMH7SVMZMnEr+oOEI0XfHGLotGuuWfcUr /3mC9auXU1m2k4b6WqLhMDE9hrTMQ0ckuo9IsETau6FdLjdenw+vz48/JdV6GxYw5aBDmTxjFiPH TgDV3ctn0ZKash2sWPg58z+dR9nO7dRWV8Vdjkg41EIkpGGgW1bq3noBdeaauz0e85pbVkh2Th4D Bg/liKOPp2jKoaRm5e6x/vUW3RKN9cu/5t8P3cem9aupKi8l2FCPpkXNH9kRij2ObVYn3tC2b+7x +gikpJKalkFWbh6jxo5n9gnfYeyU6bh70X2RukbxxrV88eF7rFjyNRVlO2mwXI9IOISmRU0L1bIk Wr94evO+ai0iQghURUVRVfOae7x4fX4CqWlkZuWQV9Cfgw8/ihlHHkv+oGEIRe21vvckXRaN+poq /vmnX/PtNwup2LmDYGMDmhY1/UpHMHoFW0SEoqAqlknt8bZ0XcZNZPaJpzLh4MNQPb691jepa2xY uZSP//smq1Z8Q1V5GfV1NTQFG+NWRWtXdn+4j1oLt6q64tZHSlp6PP4x5eBDmXXCqQwcMXa/F48u iYahRXj2oXv56L9vsXO7Gcl2BGPfIh7cU1VU1YXHa4tHJrn9Chg3aSonnnY2oyYfusf7Urx2Oe+/ +TIrli4yXdi6WpqCjUQjYWKahm7o+5VQdESigLjUBIvPCp72KxzA9MOO5Og5Z5E/aHhvd7fLdClP Y/7H77Pgs3lUlu2MuySOYOxbyISH0NB1YjGNSDhMU7CRxvo6aqoq2LBuNdMPn82cs75HRl7/Hu9D sKaC9996mfnzPqBkezF1VnAzEgmj6zEMw+hT902i6GmGQUyPEY1GCIeaCDbUU19bTWV5KcuWLeHY E05l1gnfwZOS3su9Tp6kRWPH5nX899XnKSvZTmNDnSMY+zjxG9kwMHQdXY+hRaPxG7mmsoLVK5Zy 8hnnc9jRJ/VIsNSIRVn21We888qzbN6wlsryUhrr6+IxC/t+6cv3jJQSaQ0D26IdjYQJNQVprK+l unQnyxZ/yWnn/ZCR46fsVy5L0qLxztznKdm2hfq6GiKhEIau9+kfvy8hpWweidBjaNZbsLG+jsqy UlYu/Zqzvn8F2QWDunyM+spS3nzxKb76/GPKdmyjtqaKcFOQaDQSv1cOpPuljcWnaUQs8airrWbH 1mJmn/gdTvjuufhSM3u7u50iKdGINNawbdM66mqqzTwMy8R02L8wrDe9YRjosWYTurG+jq1bNnHO RZcxaebRSbe7ctEXvPbcE2xYs5Kq8lIaG+qIhMPEYtoBb402i4eGIc3rHgmb4lFfV8Pmdas5/cLL GDJ6fG93dbckJRpbNm2ktqaKUFOQmKY5GZ37Mbb5rFn5DzHNNJ/DoSYef+AeDl+1nFPP+yH+TqRL h5sa+b83XmTef99ie/FGaqoqCAUb0bToAWld7Arb4tAsa892W5qCjZSWlTLn9HOYccwcXB5vb3e1 Q5ISjdLtxWbgMxqJp4M77N/YN3HUCDcHTCNhGupq2bZ5E2df+EOGFE3tcP+dm1Yz99knWbZ4ARWl JdTXWtnAjnXRIYkuS9hoQo+ZcaZIKERdVQWb1q1mzjnfJ6dwcG93tV2SEo11q5YTagqawSznZugz 2DdwLKZhNFkui+V3l5Vs48yLfsT0I49DcTe//aSusWT+x7z+/JNsWr+GmspyGhvqiUbCjnXRSewA tWbFe8wRrhDBxgZKtm/lzIsuY/SEabCPBUk7LRq6FqF480Yi4VA8mObcFH0LO9YRNgxi9tsvHOLp B//Gum+XcvYPrySQnkU42MBbzz/Gpx/+j53biqmtrjRd1pjmBMaTpEWso6lljKmqopyTTz+HI086 E7d37yXi7Y5Oi0ZJ8UYaaqsJh0JmnQKHPokd64hJSVOj6XdHIxGC9XWU7yxhzrkX88EbL7B04QKq yktpqK8166PozoukO9huopnDYsaYtGiEuc82Ul1VxZyzL8KfntXb3QSSEI1tWzbS2FBPzMnLOCAw rY6W7kptTRVLvvqCYLDRnKKeMJTqjKJ1n0TBDhkGhqGjRaO89/qL7Ny2hXMv+Rn5g0f0djc7LxrF G9aYs1c1c8jIoe/T4u0XixFqClJbXWnmecRijnWxh0gU7HhCXiRCJBLmwh//nMIho3q1f50Wja3F xabPKnsmm++oPz1D2pAxAIjdbNuXkYAWCVNdup1YTSXh0q1Ey7dSU7aDph2biVTtjM/07JX+JQzN xmJavFjQvjRXRHF78PYbiC+vP9n9BuDrNxBXbiHurH54MnPIzm1Z81MaBlXlZWh1VcTqKtHKtxIq L6GqdDvRqp1Eyrf36jWHloJt1Brous7ShaZFd9FPrqP/0NG91rdOi0ZMi5rVkXroJkntP4zMoWN6 pK2+QN6YyS3+PxJqQmuoIVxTSemyBVQv/5zK9d8SKtmCNPZ+TGlfEQgAlz+FlOFFFIyaRPrEwygo moIrJR1XIB1voHPT/rOGF7X5LNTYgN7USKypnu1L59O48itK168gVLwGXdN6+jR2S3w4XEaQ9XVI KVm2+Ct46G+9KhydFo0W9Qz2kZunL+P1B/D6A6T2G0DumMkYZ11OqKac8vUrKPv8HXYu/YLQtg0Y vSAgvYEnLZO8STPIO+hoCg6aRWrBIHypPTvZy5+aBqlpQKH5QjvjEoK11dSXbKF86XxKv/6A2hUL 0CKhHj3urogPy2pRgg31ACxb/BXKI/fx/Z/8oldiHMnNPXHEotdQVIWU3AKG5RYw9NDjCNVUULpy IVs/eInSBf9HrKmht7vY4yhuDxmjJzP0uHMYPPN4/IWDcO3lSuwpmdmkZGZTOG4a486+jKadxWz6 v7kUf/IWjZtXw16I77UWDiEEyxd/zctPPcilP7+FwF4eVel0PY07rr+c5QvnU1qyjXBTsNvR8jlP fUHWqIndasMBjFiM6k0r2fDWv9k273UiNRW93aVu4/KnUjjzBAaeeAH9J8/An7bvVf4O1lRS+e3X rJv7CBVLP8OI7Xn3xS6w5PF4Sc/MonDQUE4+/Ty+e+HlezWPY68tltQGx2jpERSXi9zRk8m57h5G n3kZ615/guL3X0arr+7triWNy59K/yNOZszZPyFnzGRc7n13fZeUrFxSjjyF/ofMpmzZAla/8AAV Sz/F0KJ77JiJFkdjfR3lJduZ9983ycrO5tjTzt9rmaO9JxoH8pDJHkAoCtnDijj02rsYdeoPWPn8 /Wyf9wbGXvS/u4ri9pA97SimXHwDueMP2usuSHdw+wIMPPQY8ifPYOeiT1nzwv1ULJu/x4LV8eBo NEJDfS07txfzvzfnMmj4aEZPOmSPHLM1fbfO+gGKUBRyRk7kyJv/xYzbniB7zJTe7tIuSR82hkN+ 9U9m/+FpCibP3K8EIxG3L8DgI05i9t0vMOnKO/DvgUpoNvHh2FCI2uoqijetZ+5/HqV659Y9dsxE ek80HPdkjyJcLobPmsNRd7/M6POvRvWn9naXWqD6Uhh1xmUcdfcrjDrpPHwp+1b/uoonkMbE869k 1l9eZdDs7yJce2bZCMOaWh8ONVFTVcHqFd/w9sv/QYuE98jxEuk90XDck71CSm4+h/zsD8y4/Umy Rkzo7e4AkDZ4JAff8iAHX3sXGf2H9HZ39gh5I8dx2G8fYdJVd+LLyd8jx7BnJoeCjVRXlLHgs3ks +PCdPXKsRBz35ABAqArDDj+RWXe/SP7hJ7dYTWyv9kNRyJtxAkfe+QKjjjkd1b1vLd7U07h9fiae cwWH3/EMGWOn9vh1l1IiDYNoNEJjQx2lJdt4782X2bx2RY8epzWOaBxApBUM4ohbH2Ps93+B4vXv 1WMrXj/jz7+So257nOxeTIHuDQonHcrRf36Rwadc2OPuSmJ8o76mmi0b1vLGs48Taqjp0eMk4sQ0 DjD8qWlMu/y3TLvuL7j20srnrpR0Jv7090y84jZ8+2DOxd4gNa+AGdfdw4hzfobSw6X8pJToujmh sKaynG+XLeb9119C6rEePY6NE9M4ABGKwpjvXMQhtzyMJ3PPrjXqzcxl6g1/Z/yZl/V5d2R3uP0p HHzFbxlz6W9w+VN6tG07vhFsbKCitIRPPniHdd8u6dFj2DiWxgGKEILhR57MjN//B19u4R45hjcr j6k33s+o485CUfetknW9hcvjYfIFV1J0xe9wBXpuxCiev2HVdy3ZtoW3X/4P4cbaHjuGjWNpHMAI IRg07TCm3/5UjwuHNzOXqTf+gxGzTkFRnB87EZfLxfizLqfosltx9eBQeDy+EQ5RV1PF2pXL+eT9 t3qsfRsnEHqAI4Rg8JQZTL/1cVxpPTPxSQ2kMfkXf2X4kacgHMFoF5eqMv6syxj9w1/2aIzDdlOa go1Ulu3kk/ffobR4Q4+1D45oOAAgGHzQ4Rxyy0OogbRutaS4vUz86e8ZOftUx8LYDS6Xi0nn/owR Z/+4x0ZV4m5K2HRTtm/eyEfvzO3RtHZHNBwsBCOOPImp1/wJ1dW1KUlCURh/3k8pOvUHTgyjk7g8 HqZdejPDZp/WY3kcUkp0QyccaqK2upJFCz5n24ZVPdI2OIFQhxYIxp56MUUXXtelG7jfYScz7pJf HfCjJMni9qcw8ao/kT6m40WpkkXa9TcaGyjfWcIn/3urx6wNJxDq0Iaii28kb8YJSe2TNmQUh157 F25f58rtObQkLa+Qg675c4+lnCcGRevrali2+GtKtvRMbMOxNBza4PX6OPxX95PWf2intld8AaZc exfp/ffNZQT3F/pPnsGYi65H9NBMXymlWUU+2EhleSlfffZhj7TrWBoO7ZKSW8CkG+7rVLr56LMu Z9DBya8y79CWsaddzMAjTu6RtuzYRiQSpr62miULPqeuurzb7TqBUIcOGXbo0Yw598rmZQvaIX3E OMaed40T+Owh3D4/RZfchL9fz9TjkIZBLGrHNnawctH8brfpiIbDLhn//evI7KCWq+LyMPnHt5Oa k7eXe9W36TdyAqPOvQrRQ0KsG1bCV201Cz6b1+2A6H4jGgJQAb8qSHXt+i+gClQ65wGpgFdht22m ugRqL7tUCrs+/2TOu7P4UtKYevVdqO0kIOUdcQoDpjtuyZ5g7HcvJmfC9G63Y0+f16JRmhob2LF1 Czu3buxWm/tFIFRgPiyZHkHAJfCpAr8qSHG1/POr9neQ6TE/60irFUyhyPCYD1xHbQasNr0KZLrN bff2RbPPP90tCKjE+xpo1ceAS8TPuyeFY8C0wyk86vQWn7n8KRx0+S37dPHf/RlPII1xF1zbI9mi iSMpNdWVrFuxtFvt7ReBUFsIFCFwCXAL8Cjmn1cReBWBRxF4FPM7lyJQhPmgp7nbPkAK5udeBVQh cClmu4ltxttTwNWqTf9eNjlcCvhVsw+qYlo8butc3Urzf6uCeB9TXD3bx4N/djvehBmxQ0+6gKxB vbumaF+n8JDZ5E05otvt2AHRaDRCU2MD3+63otFJS0NgPrRCCBTrwfBYb9pUl0K6W5DhFqS5BSku BZ8q4g+QKswHLNHisAXDZT1gijDbt9/UaW6FDLcwrQq3QkAVeFVhPZDWQ6nSoQXT06gKpKgi3lfT nbItC4WAquC3+uhK6KNL9KybktqvPyPPvAKEwOVPYcy5VyHU/ca73S9x+wIUnXcVak8UTJKSmKYR agqyo3gzDd1YH2efX8JAsd7+ccFQBLGaMoQWwq2AUE13wVyoGDwZebh8KUR00AyJROBRJC6PQNPN t7aaIBgexRQMTzRMpKYUYVk0ErM9zQDD7ceTlU/UkEhASEh1CxpiEtEJ8ZNAMktLKdbl8VhukWIJ gAK4FYESbkSrqwBVIIRANyRR4cKXNxAMSQzi1ki0BxcAG33m5VR//haFR8whc8DQnmvYoUPyp8wk d+JMyhZ91K12El2U2qpKNq1bzeRDuxbA7j3R6CQi4d8uYb5hn//9Nayc93a7288851LOv+3vNAgX TTGJYYBEgJR4VLMdAXHB8KsKaS7JB/ffwbuP/q3dNicedyrn3vschlDQpTStHiQZlusT1z/R7ApJ iC9jqUtTfGJSEjPaioiCKY5ea2jTFkm7PYEtAub5r/30HZ696dIWfcwbMpLr3llOLAa6vmcy51Ky cph6+W/ImDrLsTL2Em5fgNFnXkb5kk+6PephSDMgGmxsYMemNUw+dFaX2tkvfnnzITUfHJ9q+u8d sfit51g1710y3MRNdlU0i4UAVCuG4VME6W6o+vYr3n/6Xx226bECjS7RbAWY/RHNMRHFdIvsP4/1 kLsUYbpT1ghHhscMaKaoZgAzxQpwZrjMbQIuc/sW7SmmheVTBWluSHO3/dmEMIOkLqtvyh4qHpw9 8yTUHq465bBr8ibOIG1Y21Xuk0FKiWEYxGIa4aYgq1eu7HJb+4VoQLN14LPfwh0QDYd5577boLrU Cnaa8QxV2HEB8//diiDFLfCEG/n3b68mFo102KbLOq6qCCtmQKvgqTWaoTY/+H4r7uJVBR5LRMx9 zPiIVzUF0NcqwOlJEIiA2txuiluQ4YZsj0KGp+0FEJhulqrsMher+/RSJfMDmUB2LgNnzel+Q1Za eSQSprKilKb6rhUf3ucDoTYi4WHd3Vt0+/o1zL3vDjJUg4BL4FKbg6iK9eZOcQnSVMmHj9zF1rW7 Vl1VWIFGpVks3FYsJMVlBk6zPIIcryDPK8i1/rK9ZkA1za3gd5kCZrfRQsQsi8KrCgKqGdzN8pj7 5/kEeT6Ffl5BP59CjkdpNwgrbGFynuk+yfDZp3d7MmBiScD62ho2b1rfpXb2+UBoy13MB6wzL7v5 rz/HpKNOpOi409CkREowpLmvV1VIc8HO5V/xxuMP7LYtxX6oBcQUM7ZixhfAK2OE6qopW7+C6q0b KV6zEl3XycjOYdDYieQNH0PGgCFIXzphHZp0iBrNsQ3TXTJHRAKqwKfoaJU7KVu3gq2rllNRsg2P x8vIceMZOGI0g8ZOIGa0XaHcDIHYDpgzG7CvkTJwOJlF06hY+nm32onHNRrqKSveyPgpySeQ7Zer xrenGUJRkEZzeDEaCfPqvb/h19MOIT2jAEOCbpgPaIYblHADT/32mjZuSet2bDwq+AzAHnUxYmxf /DkfvPAEKz/+H+FgQ4f9HTx6HDPPuJDpp19EZmYewZggokt0aQqSVxWkqpJw6RZef+I+vnr9OZoa 228vt6A/WXn9On1dHPoGbp+fftNmdUs0EuMaoaYg61at4JjvXpB0O/tFcldnmHbkMWT1K2jx2faN 63nxb38gyyUtN8HM6chQJR89fDdb1nzbYnuP18fUI2a321W/5YpkewTuujJeuu1q7vnhaSx+55Vd CgbA1nWrePGuW7j7/KPZ8slbZLgMM6/ELUhzCTJdsO3z97jne8cz75mHOxQMgMrSEtav+KbT18Wh 71Aw/dh20/mTwo5rhEMUb95ItGnX92577DeB0N0xeuohnH3VL1GUlh7//NefY+mHb1PgV8n3KeT7 FEpXtOOWCMF3r7iOooNmtGlbYo60ZHsU1PoK/nH5GXz68tPoSS5GU1a8iYevv5Tlbz9PjhtyPWYM pG79Mh7/9c+oLt2R7Gk7HEBkDBiOv5tZuHaNjWgkTEN9Ldu2bk66jT4jGqqAOd+7lImzjm/xuRaJ 8PLdt2DUlNLPp6CG63n4N9egtXJLRkyYxPevvbHD9hUBXqHz6B9vYcO3y9p87/Z4ySoYwEFHHcf0 Y09ixITJBNLS2pTNCweDvPinm6hdvZACn0qaKpn799upq2xb58AXCDBi/CSmH3sSRQcfSiAtHUXp Mz+ZQ5IEsnLIHzm+2+0Y0iAaMVPKd3ahmtc+n9yVDIFAgCt/9yd++e0SahMewu2bNvDQnb/jprsf 4JX772LL6lZuic/Hz+64H99uFq9Z9uWnLHjzBaRsGZAZMX4SF954O0VHnoDH5UKXpojV7djEy089 xrxnHiLcFIxvX19Tzav/vIebH36GdYsWsurLT1oeSAgmHHY0l/7mT4wYPxUD00WqL9vBe6+/zDv/ upv6mqouXSOH/Zu08TPgfy92ef/WcY3ijWtJdnbLfjPk2lkGj57AOdfc3OaNPP/153j0zt/y2mMt 3RIhBKdffh0TDzHdknaDrNa/P3j7DWJay5GLwaPG8vunXmPi7FOo1xW2NulsbdIpCem48ofxk9/8 iXN+cStqqxL1y776nO3FW1m38DMioVCL7/oPHc719z7CwKKplIYNtgZ1doR0RE5/LvzJdfzwzgeT vzAOfYKBk6Z3v+CRnVIeCbN+3bp45nJn6TOBUICYAWFdcuL5lzDhyFZuSjTKaw//rR23ZDIXXHMj wZgkupss3Q1ff9Lms1N/dDWp+QOpjBrURCW1UUldVFITheqoQVCXfOeiH5HTf2CL/RrrainevIm1 Sxe1aXPWnDPJHTCYWs2gNiqp1SS1UaiJGARjBmmFQzp5RRz6GmpaJp7c7lf10nUdLRIhWF9LY01Z Uvv2GUtDAo0xSU3UoEn1ccWtd5GZu+sJOV6fn8v+8A8a1QC1UYPGmNFut+zPStsJGg0fPYaYhJgh iRqgSXOuSUxKNAOiOqSnpeP1tK07IYPVVJe3/cH6DRxktmFIYtZQcUxKs22cLIwDGVdKBt6cgt1v uAvswjyxmEZTU5CdJSVJ7d9nLA1DmlZGY0xSGzXwDh7DGVff0uH6HUIITrrsWvInTKc2atCgSUK6 +cB3xMARY9p8tmb5N+ZcE2tGqp2V6VYEbsVMF6+uriIcaZumrmbkkz9wUJvPN3y7HJeQVl0PM4/D ng7vVfZ+ESCHfQd/WjqZ+d23NAxpENM0wqEmKiuTmybfp+6/qCEJ69CoQ70mOeTMiyk6sv31O4ZP mMwpV9xATdQgGIOoBF1KFEXF5fHG/8w8VJNRh85u084bj95HxZZ15HoUsr1m+neWV5DtgVyvgk+R vPro/VSVbGuxX2ZOHsOGDWP4+Elt2vzi3ddYuewbMy3dZ7ab4xXkegQedHas3v/zNATECxq19+dR nGS1jvDndV80pJRxFyVUXZrUvvudaEjaN88lpukekxDRJY2aJKh4OeeXd5CWlYPqcqO63Hh8flIy sjjjprsJuwOEDXM/MOeYBNIyyMgrIC07D39aOqnZuXgCKQjg5O+egcfra3Hc8h3buPmCU/jqrRfx aU0M8EgGeCR5Lp26DSu45+dX8Pq/7sbQWwZMph4xm8IBgxh3+HH4U1pvlZHxAAAgAElEQVSO2tRV V3HPFWfx/lP/JFpbRT9VwxNuYNNXH3HXVRfzn99e3eG12R8IqKa4plrlClNdZuJdmrv5szSXuc3e rpK2P+DO7976MlJKaxRFR9OiVFVVJrX/fjXkKpEYUrQZ8gTTStANiWaYvr+UoGsS79Dx/OL1Rfgj 9eabSwCKQmDACJr05oLFXsuV6JeVxrjhg2kIm6MkwWCQ7OwcXIpg3EEzOPHSq/ng6QeJRsIYVnJX +Y5t3H3VD3B7vQweOQZVdVFdUUblzvaTtbLzCzjryl+iqR5GTj2UyUedyIJ357bYprJkO/+69Xq4 9fpOXhswpETu49IRSCjdKIQ9jb/ZqpDCdDUNIZASAqo5Qye0h2qE7I+4M3J6pB17+LW+ri6p/fab uSeGNeFMMyRGO/vqhlmlSjdAN8x5HYYAIwberHyEko/bKtOnWO15LTvLnoaeqirk5/WjqMisXdDU 1EQ4HGbkgHwkphVz6XU3U128nm+++BQtGiYaDsWHrLRIhI0rl+/yPFIzMvnB7x/AN2wCO0M6LkVw 5vW3sX7pV1Tt3J7cRUlASuv8E66NWxFE27tYvYQ99T+xdKNLEbgwyw5guYgxK5Acw3QOvYo5stVz 657v37jSe0A0LGtDSkldY1Nyx+/+0btIklanxHwgwh0EKyPW6IWOKQj2Qx5XGCEQ1oQ1tyrwClCR NNZW0lCymdKaKratW8WadesZniLITE8jKyuL1NRUMoaPBSGIxsDwpnDVvU/w5pMP8N7TD6GoLrRw CD3WduZpy/MVDBxVxA9/fz8FU2ZSGZHEpMQlINB/BD/914s8dsNllG5cndyFiV8fSciQ6EbzsLvb Km24L7yk7fiFalsXmKLmVyQ+q6QjmHGpkC4JSwVpdVwVZl3XBk06wgHk9eve6EkLpETb3b3biv3C 0pBSIoVAl5KwIUwxaEVYh5AuiRkSA/PBsQdOFMwRjYAKKYpky6JP+PTNl1n20TvtDnl+0er/Dzro IKZv3Mrs08/FlZEHvgCn//SXzDj9Qt5/5hEWvv4sVeVlHQrH6KmHMPvCnzDlpLMJK26qo6bIGVKi CIgYgqwxU/n13E/5+D8P8unzj1JTXopoJ2V81PhJ9Bs3jU+efbjVNRKEdFMoTeGUCCC9VTX2NqNJ sqVDE0p4KjUpcbW3TytsCwKsd4G9fULbiccQwqzV6lUED11wJNtXLmnR3ojps7j4sf+a1qVhuicK kjS3INKDNU+BeG0Us1/N52m7wGaZRvMzzSrXqCrg6WYxooguk6obuyfRo9Gktt+vLA07eeuCf7yE P6HsX1RCKGaKht5sWMQrdPldggyXZOeSz3npnt+ydunXSR178eLFLF68mH/fcxtnXHY1x/70V3jd bjLzB3De9bdz9i9up2bTKsqKN7Bj03oM3SA1M4vBo8aSM2wsrowcQjFJvQ5hTRK13ScJQpjulpTg d6VwzOU3cPJPbqR+2wbKN66mZPMGsrIyKRw2mn5DRpKWVwjAT+64D01CVJeEdUkwBsGYRLdudiEE ipRm7MC63u1ecuvml9Y/UlyWSAN+aw8R37Tj4d52n6GEts16JmZ/7HKEXqX9SLxilXXUDNPFNNsw T8DThdC9fU1aCFfi+bR3bYRAYoqK+RIyi1Qn7g9tr4m97e7wqwJdQoPW++KxWyu5Fft8IDRmQMQA n1VfRtNNPyyq2xW8zAdQt5Kp7B9AwXRDUlyCDBHjowfv4pX7/titvgQb6nnmb3/kkzdf5qbH55I2 aBRB3XxQvUOKGDa0iJGzzW0ldpxF0hC1Er0ssZBWoRxFWA8TkqhhVkDXEQSkJDBwJKMHjWTcMebt aUhz3+qoEd9HNyAGaLoZz7DPXWDVRbVuaLvqmS0g9h9YNznNwqUnBCFlQnuJQUsbWyhEwjYCqzo8 zcFZAzAM0K0d7GUj3B2UJlQE+BXQrJETRcp4AaVksM9BkSIuXHbhIyGar1PitYnva10Tw7om0rom 9jygjq6Jec6i5bVL+C7+J0EgSXcL6ntZOFpPcdgd+7xogJkZKRXzYYsJK0MSibDWDzAkcZcE7LKA ZqQ+XWi8f//tvP5w+5XGu8K2jeu44wencvNTbxAYNBpNN/tju0E2hrTcBatfLiFQhMQtwUC06Ld9 E9thGNslo8X7zdzGQMb3UaR10yugSjAUgTAkCFAQ1kJPZt1Rl1Vf1S6ODObxdSniWadRw3zD69aD Kml+sFyiuQ17JFRYeSyKSPiz2tYB3RBxwdQSrA1VmIWT29MBBfC5BFFrW/v6qaLzK8cZmPeNIc0s 3Zi1/ISkWSRs4XJbRZyVhNwQXZr9jhmm4GvSDLAbdhuY19Wu+WoLhy28iaJhY8flYtKOzVi/r/3D 9xKKKzkZ2C9iGmEDiEkCLnPNESFEXPGRrZYDsG5ojxCkuGDRa8/zxsN/a3dSjhCCsdOmM2rGbCZM nkr+oKEoLpWG6iq2rV3J8hUrWPb+6zTW1bbZt3TrFu67+gf8+sWP8HkDRHVpLtCkND8MEqyHUCbE Y83bSLcEJWY9oFgPkU8Bv0ug1ZRTsnktW1avIBwMkp6Ty7CiCWQNH0dOWpppWWkaaxbOJ2PEBNIz s2jC9PljYJnyZklCnwJaQy2Rih00VJZRWbKN2spyfIE0Bo0cjTctk/ScXPyFQ4jokqaYGSPSZPMD 4lXNtvyqGYtQhVnE2DoUoWCQmuJ1bFy9kuoyc6i5YMhwCkYUkTF4NNLrMpPorDKHbsWMDdz75ucE Wt2FOlAXE5a1JhFCia9509m0jZg10haKmZm+YYN4iUW7rYDLdBNCtdXUbllL6ZYN1JTvxO0LMHzC FDKGjiUrO5eQdU0iuh3fMMXMo9jLZJq/uyBhhI/mqvVCNL9Awrr5FzGaX3h+VdDYXqCuAyrKk0vG 2iVC4EmysM9+E9MIG6BFJR61pSwrmKX47P8WVrFen0ug1Vfzzr/ubNfHHD1pKj/+69MMGj6SoKEQ 1pvjARmDBP2mHcVhF4L84/189MS9/Pvu32O0KgO4aeUyvpz7NCdffCVRl+TaI8ZQU76zxTazTjuX a//2OGFdsmXVNyz5+H1KN6yirGQHW1ev4IRLr+WUK39lmsp6lDWffcC7T/yD1Qu/REoDQzffbUII FEVBKCojJ0zm4OO/gwik8+wfbsDj8zPtuDkcet6PGTB5Broi0GMaa//vTdZ8+CbrFs2noaoCaRjW MJv5b7N0ofl6FUKQnV/IhOO+y7HnXkz2iPGEdPOmVgR88dR9vPP337W5jpfcdi/fzHuP5V98TCwa xTD0+PUWioKiKKRmZDH9lLM49kfXkpk/BE1ay1EogpvPPprNK8xAqO06TJw5i9898w6blyzgtgva z+hNhkBWDje9tRh/aiZgxkVcGCx95yUWvPwEa5d8hWEY8esDAkU1+z566nQOO+9yJp9yDlHVFGUp Tatn0YuP8Pqff9nmeA9+toq8/oMor65mxacfsG7RfCq2baF0WzGarnPpY+/izilE6jLuYidDrL4H yiIIc6EtIQQZqckVLN4v3BMbHdpN8olJCLhE3Id3C/ONuPyD1yjfXtxm+yknncVNDzxNBBclEYMm 3TDjIVYAUMEu9gtpbhdzfnwTBVOO4p7vn4Deamr8h88/wakXXExqShpS14hpLSPRkaYGPn7hcV55 5O+UbNmEHmtZ7SvdJenvU6mrq+HJ237BR6+90CZ7FJrTftF11ixdyJpvFqEIgWEYhIONzH/jRRa8 PZepc87lorseAwmL33mZb//vzY4vqJQYsvlYFTu2Me/pB/j02Yc589pbOOEnN1nDwgKfMNqcG8Bj v7223ZqqANIw0A2DuqoKPvjPQyx5/00uveshxh1xArqU+F0KRiyK1qpdPaZZ82xku8dMFqlFyXPp uDzmC2Xnum956PpL2Lp2ZQd9N6eOG7rOqq+/YPWiLxn6xN+44q9Pkzt8DFHdzC8JdNC/0rUreO7O X/HlB+8RCYdaHCNv8DB8KmZ8rotuiVbXM7VU7BdRekZGUvv1iVmuEcsPB2vURBEosSjL5r3bZtvM vAKuv/PvhFGpiBhURyT1mqQpZtAUM83ZYMygUTOo1SRVEUl5VHLIzMM448e/aDP8uGXtSjZt29bh Wixf/u9t7vvVVWzbsK6NYNj9NSJBHv71lXw497l2BaNdrGy+RFxuN0edfr6Ziu3zcOyZ3+tcW63Q Yxqv/PX3LH7hQXNOjUch0MHaCB0JRntUlZXw799cRV3pNjLcCqkugdKBySlEDy7HoLrwZOSS61Oo WLWYv1xyKsWrV3S679Iw2LxyGX+5+BTq1q8g36eS51VI6cBXuu1H5zLvjVcINwXbPYYdA+nq6Wll W7u4p4ltYSiKitvtITs7uWSx/cY92R22aWtH5kUsTE07GZYzT78AX1Y/doR0GjRzuDJGc0KY3TUh zag90h4eNDj+oh/z5mP/MLNA4weWVKxZBkXj2u2Xsbul9CT899UX+OztV9t1o9weDwOGjcTl9lBZ tpPaio5rH6Tn5jFt5uEYihkPmDTzSAoGD6WiZDuTZx5J1uBR5GSkMWTMOHx+P00N9axavZqytStY 8lnLtUKlNHj+b3/kxLMvIiUjc5cLVNn4Ayn0HzocoShU7NxBXTtzGsq2b+WTZx7iit/eudv4RBJu /i6RmJZBfX0D91//I6rKWrqQaZlZHDbnDGbPOZ20rBwa62pZtHAh8555uMW2VaUl/OOai7jnpQ/I KCiko8qLuxvCtEdPukqoIrmp7O0hhEBVVdxeL4GcwqT23a/ck11hCwaYb6lIYwNb1rRdBGngoMGE dDM4plmpynZUvHV7EoFmSCJW8Kqw/4B2qyatXLSAo89IvhQ8QEMwyKdPP9zmjeT1+/nO5T/ntB/+ lPT0DHM0QDfYvn4Vbz7yd7547402prHb5SYrLY1GzbwjfTm53PDg8+TlF6AZBrXbt7Bj41oaqquJ pqQwaPhIxh1yOOkDhrF93Qr+fPm5lG1vno1bV13FplXLmTiz4zU/3R4vR552LidfcDGFYyfjcXtQ FZC6zudvvsjDt91AKBhssc/qrz4h1FBLanomHZmcugHZQ0dxzf3PABKvapYaSNQZXcLyBZ/z7tMP ddg/RVUpOuwYAi7Ba889xtb1a1p8P2jkaH7/1GsUDh3RotrblCOP55wfXMrvrvwRaz7/IP75tg3r eOaBe7jq9ns6POaukNIMqKqSdqdD7I5QQz21Zd0XDUUouNxufP4AubupO9OaPiMabWjHfIeu+2N2 rKM9dmtNAOlZ2RTNnM3wsRMYd/B0UvMKcecMor5kE/PmPtNm+xO/dxmX3HArYalQFTWISTPiP2zi wfziH0+R+9c7mPuPO1vsIzCHAVMsuz5YuZPVX3zII+++TvG61RixGDFNQ0oDIRRcbheKqqJ6vEyf fTzpA4e3EA2AlYu/2qVo3PDwC8w4+gQaDJU6zUDTwSPNpSKOP/8Stm3fwdz77mixT3nJDhprayzR aN/ciBgSJT2HcSeehRDmCEOWW8FraXZZdS2v/u02PnjhqQ77VjhsJBfeeDtHnnw6SjTEkvffaLON PyWVVx76a4dtZHhVhGg5SfLT157jgutv63Afm6Jph9B/3EFMmj6DjMLBiLQsQsKNmplL1HpRtV/2 qWNiwToiVd0bPRFCIBQFl8tNIJBCYf/kptrvF0OunW3OVm4pwZOaxpDRRRSvazmXY2d5OT5F4FXB HRPEsIYHWsm+sMbfXUrzAtBVlZXtxhyKphzSYb9SMzI56+qbOebsi3CnZhKWCjqmC5XhVqhdVUFt RdtK5IcePwdFUQlGzFiLJiGmSLy6Qo7Xzdix7btDtnB88tEHPPWbqyjdWoyUbcVTWittARAK8fEb L7fbXqixvsNzA5h68KHUGyqVEbOQUcwAt2rmZahCYcSotiX3q8tLCTbY7XZgaUhruFo38xjcgOEC XdP49L9v8u+7b6Nk07oOXDovx37vR5zz0xtIzR+ArkC4vobVS9pmAq9btoR1y5a0+XxX1NdUs3np /A6/nzL7RL5/420MHDWOsOKhUTfrvIQN8OhWRrBh3XISwkmYHHpDLdHKnbvfcDcoioLb6yUlI4vU rPyk9u0zMQ1o9hVjEnD7yCoY0EY0Frz+HN/76XVkeNLQrOc/YkhcimjhoqjCHFbzqYJ0F2S5Fd58 6Qm0SLjNcfPHti2kYzNt1vGc++NrqdGgPGLQpJuJWR4FYtLAnZqJPzWNpoaWD2e4sdEM6gpzJMcw 7FXqd38dln41n7//7HuEGpKb8twVooakLmpQr5kTzQxrroaCoCEm2/fdpZ1mBR3eCFZsSlfM8/a7 BKVb1/Ofu29nwXuvdziqMnLiVC7+1R8YPfMYQoZCUDPwqoJITHY+yNwJVi1aQFZu+yvd/fxPfyd9 4HAqwga1YUnYShAzyzc0T6o0rGzmJGLJbF/+dbwkQ5cRAtXlwuv1MWrUqKRTbfuce2LYmXteLxOO OJ5vPv2/Ft9X7NjKP+/4DdfceT9g4FIgpIt49qAdUFUVM48gxWUuaLRmzSpefuDuNm+2/kNHMGzo 8A77o7pUmnRBVUSnTjPjI5LmeTPevP6kZ+e0EY23n36QkdMPJycrB1WYiVZeRZCiClYv/ZqXH/57 u8eLRKLMfeS+NoKhqCoTDz2cwWMnUTRlGgC1VVVs2rie6i3rWbHwy3YFcXdE9ObRq5j99rRmHNsJ Yl3BKwQBl4JXSohG+N+zT/PWA3dSXVbarnXhCwQ479pfc/LFPyMlJZXaqEFUNmfTpnhUfIGUFktJ dIdQY32HooHLTXXELDTdGDMtC/veslPRpTUtINm5Jw0rF3Sr3/Ywq8tlxjOGtFPCcnf0GfckrJtT rA0h4pl3E085h8xH7m2xBoqUki9efJxI+VYu+eO/GJjfn/qYJKKbs2jt2IXHqrHhUmDB3Kd4+Pab CLWzXOLR5/yAlPT0jk9TQpMuadLtB8zKBBSg6pCZXcjYGUdRWry5xX7ffDGPG+fM5LCTv0v+4JG4 VahuDLFlyecs+eTDDm/+YF0Vm5d+2ebz71z8E6645U+4/f4W73ZDgh4JUVrTwL9vu5ZP3prbZt9d YafJ2+nVBs2p8Ynp1MniUSFDCL5dtoR/33Urq+fPa3fIWlFUJh02i0tv/Quji8ahqCqaYaaGS8yX qE8VuP0pjJo4hRVftZzDPGHGEfzywRfM3IldENahZP0qho0pQgiB1+fn/Zf+0+62UV0SjpmWV9Sy MuwcILDmCUlzpmsytk9TbRVlG9oG95NFEQoer5eUtHQKh4xMev++457IRPfE9CEzcwo48YrrefFP N7XY1DB0vv7wf6xccghTZ85i5OHHMXL0GLLzC804Qn0t24s3sXb1Kha/+zKlxZvarHcCMGD4KE74 /hVohohPr26NORlNxtPFE4d2I9bb+MhLfsGyzz6kJqGOqDQMyrdt4fVH7kvqMsSiGo31bd2S3H75 ePz+Np8rAhSfn+xAuP2SA7t5DcY9jXg8ScZniMa/7wJaOMQLD9zNa4//i8a6mna3URSVI087i+PP /QF6Uz2rFze/hXXZXLTHBahuF+OOPKGNaKxa+CWP33Y937/uZgaMGtfG/ausrmHxZx/x/tMPsm3D Gu7/7wLyWy1H0ZqoNP/igmoVFmqIyTbTHpKhbvsmQtvWd3FvE2G5Jh6vj7T0TAYNGZZ0G33GPdEx FVyoEiHNilVBXXD4uZdSsWEVH730dKs9JMGaKj5/9zU+f/c1vH4/bisH39B1IuFQu282m6x+BVx5 zyOIlExqogYZ7vZV0FyKwJpkhi1s5jRr3TAtkOwhI7n4d/fy9K3XUlPWvSBXSkYGQ8eMZ+03LddT eenBv9Ko6Uw95FDyBwzC5XZTX13Ntk0bWLNmNV++/hyV7eS1rFv4BcFox9fBtjB29X377PqtEW5q 5PN3X+9QMMCsqP3lf9/kq/ff3mVbACnZudzy2pd8/uoz7NjU/OAZus4nb7zIoo/eI3/wMEZNmERK eiaxaJjN69awZd0amupq47kXLzz0d6669S4UV8emScww4xRx4cRcXqO7RdRKv/4QPdq2qn1S2PEM n58hw4bjCaQl3USfEQ0wU8w9VkBTMyAUk7hcKZx+8z243B7ef/bRDveNhEJtVjrriNz+g7jyvqfJ mnAoNVGJKsxJVe3dFFKaE5gSK2rZZnxMmoHEYEwwctYcfv3sGN546K8seudlwqFdl2Dzp6QSCja2 +EzTY2ioHHP2RW1Eo7GulpfuvZ3XfT48Xh9CmPNTwqHQLgOEqxbO55Hf/pyswvbfrpphJcclnrP1 /3aco31kq393ASmJhjsXh3H5w+RlZ3PDXx/mD1dc0Kb4UrChnk0rl7FpZdt1ehN5/98Pc8y5lzBy 7PgOraioNWnQ/r51GcauoEVClC/5tFttJMYz/IEURo/rOIC/K/pMTAOah+m8ikQXprXRGAPcqZz6 q7sYOW0Gbz/8F7au61pJPV8ghSNOO4/jL7sO74AR1GhmEpJZF6L9EfeYNN2QxDeybW0YCLNeiC5p UgR5Q0fzi7v/RclVN7Lov6+x+puFNFaW01hfixAKaZlZ+PMGMH3mYXjyBvLXK85ucaxQfR1VW9Zz 0nk/oGTjWt555vE2owzRcLjTDxpAXkF/TrngYr754pN2vw/p1rTzVhXApJREpSDawdNiWPVc98gw WntIA6mFGT7tMH792Fzu/+11FC9bmHQzuQMGsWPnTvJGFMXjU60J6xLZKo7RXYLbNlK7Ormh4fZQ hILb4yElLZ38ISO61EbfiWlYhGISl1s0T880JDIGMdXHmFMuoGjWiaz8+F0WvPcGm5Z+SX1N9W7b HDlpGiOnz+KI0y8ga9hYgripjcr41PGYYdaVaO8e0gwzLmAHWe3IvwFmAppVL0KzxGXH9i0QqmfO j67hHLdKTIsQ0zQEApfbjeLx4XG7WPl166KEEKyvo3jDWgaOm8r5N/+ZsUedwrtPPsDGxfNpaieI ayOEYNDI0RQdNJOU/AG899h9hIKN5A8exm+eeo384UVEPv243X2bdMN6QBLyZDAFXNNlhzkItZqk MmoQa7eyPIRjPftekYZOTX2QJsVHRtFB/PKpt1j69kt8+fZLrF26kNguzP6U9AyGH3QYBx87h6Lj TsObkUtt1CDYQZ57SJe4DGlVK+uZ89j08Rto4eQKALdGCIGiqni8PtIzsxg2LPkgKIBYVNq5UNUd 11/O8oXzKS3ZRrgp2G62ZTLMefILskZP7FYbHeG1siLNoVNr5qtViMZv1YVw6Rqx+moqykoIl2yi vqqSzWtXEQ2H6D9sJJk5eWQPGExg4Egys7LAnx6vQxppVSPDrs8QqdyBFx2XFQiMSol0BxAZuaYZ L1umrJuFXMyFgVJdCqmxJh7/+YVsXPIlKVk5jJk4lf6ji/AXDqOfVTpdM6B851a+fOtlNq1uGUn3 eH3c9OgrDJt5LE3W3Sq1KMHqcmq3rKVm53bqqyvYsXE9/YePIiUtg6zCAWQPHEp2fn8CaRkYQmH7 2hUsnvc+U4/7DtlDR6NLiDTWE66vjQ/pa4YpCL68gUSlEl+O0pAynk/iUUCJNqHXV1ml/awaGVKS mleI4vYQrNyJ1KJWYR6r3oXbR2p2Ho1VpRDTUBVQMet3dPZdExcuwwxER6VCILc/UjFjES5hLlmh Rpuor66idv1yKrZvYeO33yAQ9B8+krzCgWQW9CdjyBjc6TnEXB4iloUkBOhNDWgN1bisgkcxKYkY Ek/uQDShWLkZ5j3TkcB0hqbqSv7v56dRv/HbLrcB5rC7P5BCfuFADp11HNffkVyQ3abPWRpgvrGJ SXOhHcOsgCUNuySgpCkmcCtuPOn5ZGXm4xo7FVXAFGF2TFqjHDrmQ1onQYvIeAWo+BCjtCtpgTQk 7twB8TJ7AC5p7qMl1ATd1a2zZN67rPziQ/RYjGBDPeVbN8M7nT/vzIL+ZI6aSLUm41XbFTy4cwaS mzeQgoSKW9CyanuDhLoIgEQdMoGDL56AAVRFTAtJeNIQeelx0VClxGuYwqjZ52ZZU2ZRY7PyldsT wJ2XgqI0lxoEaDTMIQYloyC+BgrW9zGgJgYiszBew1PSXC+0s0jrNxQGKIakUYIRM+L1QV0x8KgB 3HkB+hcMYhBwsF2By7o+ZsUuCFn1XOxYhQAUbyouX2q8VKAKeAyJZpeetK5JuJsBjYoVC2jY3DWX 2qZFfkYghaLx47vcVp8KhCYSsUq1+V0CjyLjC/DoAjTMN4Jir79h7aNYpcDsoVtJc2Vvw2jOQbBv BhsFYQYDdYkmwB5wtN92tsDYYqRZqdaJNFZXMu/xv+1yxGZXeHx+TrniemRGHo2atZyD1UfVeijt QwrRNijbeohUIltsI6z/Sig0Hn8oDdnSf7fbRJqxi5iQCKPVvnZfsK5XoiBY39s1R+0+dwW7oLFd WjHxIBpm3owqBEqsucqWXYELmn9vvZ02BLYIy/g1bXFNsHJVuqEZWriJda8+huzE/KbdEY9npKYx cERRl9vps6IB5o/XGDPL8Jnrg9oFaoU59d2qMZr4BkzEvkHst44tFJJWFoM9hIpZs9O+wZuzAGW8 9J9dMT0FgT1h1t5u3HHfJZCeQcWWDZ1eOCmQlsGoqYcw63tXMOTwk2i0p/vL5n7acYP2zrPNubT+ LuEBV4RZMiDxmtDq/CT2miaWhWLFepSEgjOJ7doParudkHTwRXK07quNWbHdFDVoOyEx8drYmZx2 G4llG+1za32cnqg2XvbNl1SuaJuslyx2PMPr85OZk8uwUWO73FafFg0bXVriASDMmo4C06+NF8ht 9SqTND8tEcMOeMp2hxAVpdW6Hy3asdtouW9USnwJxYXd6Tkc9qNfMvviqxBN9TRUVxIuLaa+vISK Hdso29ZceCWnoD/9Bg8lY8BQcoaPJSU7F80VoDFmCoYdX7Af5myitTUAABOwSURBVMQ+tVjbI+Ec oWURZI9iisCusHe1r4v9LhQ01xUVQrYryPb+e2nspAPpkbgTXbYEywZaCox9D7RusaPf2y5I3B3B 0MJNrH7xn+iRzqUC7BJhBtL9gRQGDBlGWlZy0+ET6VNDrrtDt45rB6USq3LvqkO7Mwx1KzCaDLZY 2DERe39d9ePJCJCRVUDuqAnNJfYR8WrFtqmsWa5OnSHRNBkPtsZdIazU9RY+dcf9jLsVmK5MVw1i 261r2heWdtsNLe8B6Oj69MbKbjsXfkzFN591ux0hBKqi4vF4CaSmMWHi1G611ycDoZ2le+M/3UOX ENXBp5prnUhhBlPtXJPWsRZ7uQawXaWW/rY9z8O2MKLSzBfQuniSB8ryh715D+yKaFMDq56/D6O7 GaC0dE2ysnMZvd+Kxr7/EtrjNOlmLMSrWmP6Vs5Ge7GWuFeR4FHYN3xi7MS2LkL6vr5+vMOuWPPG 01R9m9xKgO1hF9xxezwEUtMYMHgohYO7ltRlc0BbGr2NxBSOiG7GRexV7BXRPHUee4GgVgqQ6G/b Q54RwxzlOVCshL5K+YaVrH/pn8geqv+hKqaVkZGZzYwjj0You5nSuxsOiEDovo65EllzXEOQuNTf 7u2Fnso67CrSkIjOVB522C1aOMTqJ/9MqHxHj7QnFAWXNczar3AA4w86rNtt9t4SBg4dYscmOvvX 225I7ZKPadrSveQjB5M1bz7N9i/e65G27ACo1+sjLSOLKdMPIyOng8JBSeCIhkO3CdVWsvKfv6ah rHO5JQ7tU7J8AWufuRfZAwtEQXPtDH9KKnn5hcw86vgeabdPLJbk0LtUlG5n7fwPWfDXG7s9qepA paFiJ4vv+xXhqo7XtUmGxBGTtIxMJh80nf5DuzZBrTW9JxqOC9xnqCkpBqB8/nusevLPbZaudNg1 WijI8kf/QP3apT3WplAU3G4zlpFfOIBZJ3yn2wFQG8fScOgxpGGw8sUHWf3Wv3u08ndfJqZFWfLE n9nyvxfbLZjcFexYhs8fIDM7l4NnHMHgUV2foNYax9Jw6FEMLcK3D93Kho/fxuhufbs+TiwWY/mL D7LxlYeQu1nKsbPYbonb6yUtI5OBw0ZwzJyzeszKACcQ6rAHiAUbWPbX69j0+XtIRzjaJabrrHz1 cdY9dVePZH3amGu0ukhJTSM3v5CjTphDQRcqju8KRzQc9giR2kqW3n01Gz9717E4WhGLxVj12pOs fvR2YqHG3e/QSWwrw+cPkJGVw+hxEznqhFN7rH0bJ7nLYY8Rqalg6d1Xo0fCjDr29HYXzz7QiEWj LH/5QdY9eVePC4ZQFDwe0y3pP2go3zn3B/hSM3vsGDbO3BOHPUqktpKlf7mWSE0548/4EarH09td 6jW0UJDFj/+ZTXMf6lGXBEzRcLvNgsF5Bf2ZddzJjJkwrUePYePMPXHY48SC9Xz74K3o9dUUnX8V vrSM3u7SXidYWcaSR25n6/9e7LGgp40dx/AHUsjK7ceEyQdx4hnnI9Q983g7Q64OewVDi7Dq3/fw yW2XUbNlXW93Z6+yc8VXfHTTeWx999k9IhiKquLx+UjPymboyDF898If4U/L6tHjJOIMuTrsNaRh ULHgf3x68/ms/+iNPp8EpoVDrHjlEb645SLq1izpsTwMm8Q4RmpaBgX9B3HyaecwbMyeqfJv4wRC HfY6DVs3sOiPP6H6lO8x9oJryOg/pLe71OOUb1jF6ifvZPvn7/a4dWEjhDBXS0tJJadfAYcecTQz jp2zR46ViCMaDr2CHg6y/tVHKVv6KUXfv5FBR5yCLyW1t7vVbaJNDax98z+se+EfhCpK9thxFEVB VV34/AGycvIomjiFU8/9Pm6vb48d08YRDYdepX7zWhbeeSUbp81i6sU38P/t3VtQW/edB/Cv7neJ qyXuRtgoAdtxHBK7Thtir+ok65L1xpa8nSTbyWyh7T51dnjwPhledvzA0+5LB7ZPndnMQtLpDLvT zlbThmyS1okzXTuALRKEwQbjC+YiQHdpH3SxBMLmDxIX5/uZ8YwHcf7nf4TO9/z+5/x1TnHjC5DL d98VlpB/GXe+/Bg33v833L/6WU4eObCW1JfRNBoUFBWjunYf3nqnFUVl1XlbZzpecqVtFw0F8ODy 7/HHa39C+XffgM3xUxTbntsV4RHyL+Petc8x/P6/4v5fPkY0R19rX0vqxKdSBYOxAJaKarz25lnY Dubn8mo2vORKO0bYt4iJ3/dh6pPfovw7p7D/rR+jYN9BaHbgJdql2Qd4MPg5Rj7swYOrnyCS43kX 2aTu96lQQm80YU95JU68/jdo/utzQA6/W/IkrDRoxwn7FjHxh1/j9v/+F0z1h1H7/XOo+s4paCyV 21p9hAI+LN+ZgMf1IcYH+rE4dh2Ibc39zNMDQ2cwomRPGQ6/cBSnHe9syXmMdGKhsdFn42VtK3dN 0dMpGgpiduhzzA59jsFf/gtKDx1DadNJWI58DwZLNVR6Q977sDT/EN7Jcdy/8gdMffER5oYuIxTw 53296VYFhrkMB4+8CMd7P4PWmL/5GGtZd2gkn8wlkUhyGx5E6xD0zmHy099h8tPfQa7RQWdtgGX/ IRgPHofl2cOQ6wyQ64xQabQbXodv0YuIbxHhpQXc/stnWBq+jDsjX8E3fmPb5pRkC4xDR17C2z/5 OSzVuf326nqtOzTkCiVkMtmqxxdu1OKdm5AolDlrj7ZecrJS2LuwpesN+5YwP/QF5oe+AH7zS0gV Sqj2VEJdWoEicznUpZVQmKsgNxVDaSpCUUnmzXQlsRju351GaGEG4fkZhO6Ow3f/Dmbu3EJw5g4C 924jFt3+xyil7o2xIjDe/dk/oXxv/bb1a92hUV1Tg8Ev/wypRBpPP4lkUzPcBv757Q0vS5QuGgrC N+mBb9KD2e3uTI6kLquq1NAbTSjZEx+SvP2Tn29rYAACoVGz7xko1WrIFQpIJVJE+UgeorxITtxS aTQwGAuwp7wSh184Cud7/wjzJp+OlgvrDo2qvXXQG4yQK5SQSKWbrjSIKFPy/IVcrkjc37MYlopq nHj9TfzA8S4023DSM5t1h0Z5TR0MBUVQqdWQyWTI03R6om+l9ElbGp0ehcWlqNpbhzfOOPDKG2e3 /LLq46w7NGQKFWpqrRi9/hVkcnm82ojFWG0QbULy/GByOKI3mFBUasazBw/j7LutqD9wZEsnbq2H 0DyN+obn8KePXFAolPBL+FAcos1Iv5yq0epgKiyGubwSR7/7KlrO/whFlqrt7mJWQqFhqayBVm+A QqmCVCpDVBJhpUEkKFldSGUyKFVq6PQGFJbsQU2dDafPOHHsr05DrlRtdzfXJBQae611KCwqgUar w6J3HmGe2CASkgwLuVyRemRiqaUcB557AX/7Titq6nP3UKN8EQoNlb4QVdZ6TE6Mwbswh3AwiHAs hugOmAhDtJOln7tQqFTQ6Q0wFRajvGovmk+dxmtnzuflzuH5IPyFtdNnf4iJUTcW5mYRCgYQXY4i xhOiRFmlD0UUCiXUGm28ujCXo77xIN48/yPsa3w+p09Ayzfh0Kiorcfrb/0QszP3EfD7EA6F4pVG NMrgIEpIDwu5XAGlSg29wYiCohJUWPfh5KkfoPm1Fii1xu3uqrANfTX++Kun4Ll+DUuLXoRCQUSj UYRCQQYHPVFyZ8r40mNapbqbPz/p2yaTyiBXxMNCq9PDUFAIS3klXjr+Ck6cPgtzlXW7u7thGwoN qUKFM+/+FLcnxuOhEYmkAoTBQWuRSqWpGY8yuRwymQyxWAzRSAThcBjRaCRetSZCZDd8jtKDIjn9 W6FUpq6KGIwFKDFb8PyLx3DijTOosD6zq4Yi2Wz4JjzGwmI4/v7H8P1iCQAgvTeNJe8CgsEAohFe iqVHUtOjExOYNFo9dHoDtDodgsEgAgE//MvLCAb8CIWC8SFvJIJoLJr6tulOqERW3h5CkqgopLJE VaFUQaXWQKPTw1RYhBJzGV48ehxHm0+hvLZ+14dFkuTK9Ob+CiNXL6PvV/+Or4e/wsy9aXgX5hD0 +xGOhFf9wenbI30HS05g0ur0MBUWo8RSju+dPIWXT76Ob4b+D3/+9GPcv3sHcw9nsLy0CP/yEgIB P0LBIMLhECLh+GcpEk0cjLZoOLMyJKRSKaQSKaQyWaJSkkOpSgSFVget3oDCohJUVO/Fy6/a0XDk GAyFpXnr33bZdGgAwMLsDH7zq1/g8icf4e7UbSzMz8LvW06cJM0sOQGGyNMo21FYKpFCJpdDqVRB qzegqNSMfbYGvPl37+HQSy9DmnhsYCQcxG3PCNzXvsTgV1dxd3IiHiCLXvh9ywj4fQgGAxkhkqxE Vn62gI19vtLv67JWSCgUytTQQ6VSQ63RQmcwwlRUjKraOjQefB7PHDoCc5UVEsn2PYcs33ISGgAQ Cwfx2Uf/g99++B+YunUT87MP4VteRDAQSP2hI+FwvORM/pEZHrtfYmdbuYPJ5QrIFfEJTHqDEUWl ZjzXdAxvvf0PKH7CrfZn701h7OvruPXNddwYHsLMg3tYXJjH0qIXvuUl+H2JoUwiRKKRCCKRCGLR KGKIiX2+EgGXDDmJVBo/N5HYBqVKFQ8JtQZanR5avQFGUyFKzWV4trERFdZnUGdrhM5UlIt3c1fI WWgkTY6N4L8/fB8To27Mzc5gybsAv285ccTwIxQMxAMkyvMeTwPJihOACqUKao0Wao02ftXAVIBS SwWOv3ICR0+ehmID06O9D+9hzPMNpm9+jRtD1zB5axze+bn4UCYRINkOTuuZdJgMO5lcDrlCAblc AYVKBbVaA7VGC43eAJOpEHutddjfcAjmKius++qh1u+8O6RvlZyHRlJgcRZjnlFM3xrH18NXMT42 ioW5h1ha9CLg88WPEInSknapxBE6eRJQq9NDbzShrKoGzzYegqWqDrV1VhiLLUAOy3W/dxYTN8cw dXMEN79xw+MZxbJ3IR4ifh+Cfj8ikTAikcjjP18SCWSJSVfJoNDq9NCbCrC/3oZqaz3Ka+tRVVML hVqXs/7vdnkLjZUioQCmxkdxa2wU46M3MH5zDKFg5oNlYrHYqnuGbtfPVtpJfdsp2w/Ej9SWsgpY 99tQWbsf1dZ9UOm29igci0aw+PAupianMHP/LpZm72Jm5gG8CwtYWFpGMBC/oheNhCFJzZ9QwaTX wWgyoai4BNoiC8x7zDCXlUFXaN7S/u82WxYaRPR0eHpP8RJRXjA0iEgIQ4OIhDA0iEgIQ4OIhDA0 iEgIQ4OIhDA0iEgIQ4OIhDA0iEgIQ4OIhDA0iEgIQ4OIhDA0iEgIQ4OIhDA0iEgIQ4OIhDA0iEgI Q4OIhDA0iEgIQ4OIhDA0iEgIQ4OIhDA0iEgIQ4OIhDA0iEgIQ4OIhDA0iEgIQ4OIhDA0iEgIQ4OI hDA0iEgIQ4OIhDA0iEgIQ4OIhDA0iEgIQ4OIhDA0iEgIQ4OIhDA0iEgIQ4OIhDA0iEgIQ4OIhDA0 iEgIQ4OIhDA0iEgIQ4OIhDA0iEgIQ4OIhDA0iEgIQ4OIhDA0iEgIQ4OIhDA0iEgIQ4OIhDA0iEgI Q4OIhDA0iEgIQ4OIhDA0iEgIQ4OIhDA0iEgIQ4OIhDA0iEgIQyNv+nDBIkFTa1/WVz1dB9BkOYBu 90baXbFcvxNNFknqn7NraKOdFufuhHND20G7FUMjrxphHenMskP1oTtXO3a/E02tw2gbiOHKdAxX pgdh7z+wdcFhu4je6UG02bZmdbT9GBp5Zq0HXP0rduD+PrhaHLBvuvUhdHf1wd6TvtM2oq27A+jq hGvT7ROtxtDIM2v7RVgzduAhdHcNo63dkfmL7k4404YYF/qzv+bsGk77+QdwuR2wt6xYqe0c7LZh eNxAcjhzofUAmizOeD8y2uvMGO64Wh/14dHQKjEk6nJmGQI9Gi7Fh1xZls/YtkQfsvVrzffDCReG 0N2ctt5+J5qaO+FZs33KF4ZG3jlgb+mDKxkC7g/gggP2jHK+DxeaO2DtSQwxBjrgaU3uyJmvXUJf 5k5ha4Q163qH4BlJ+7/tP3Fluhf2J7RnT/Zhuhf2/vSh1RC63Y74az0OeLJUMtb2wUfLwoFLPY5E //tgTw6fepA2NEvv18r3Y+UyQNtAbyKA+3ChFbg0cBHWx7ZP+cDQ2AL29g54uuJHRVdXR7z6SP8F 9zA8SKsYbBfR1pLY6d3D8Ng60JZ4zdp+MXMHcw/Fj7arNMJa/+j/9pbGR+t6XHupk6orj9iNj6qj FgfsSFYyq7lanUBPIgjcw/AkqoRk9eFxD6faTPVr1fuRbRkHLvUAFyzrbZ/yQb7dHfhWsJ2DHefh 6gdcIx24tHI4sZl2bR1w9SNziOL+AC53g/jJSXcnnK3ApekY7BhCd/N58T71O3EBvbiSsY0OXMpS TTxetmVy8bu0Waw0tkQj2tob0N3aAbScWz2csDXAivQhTCe6+xOVh60BVncHuhOvZQ4L4kd/V2t6 VRAffmBlBZG+rrXaGxmCJznccX8A10YuB7ciMSzJ3LYnDxnSLiWvuUyi/eleILnN626fcoWVxlZp uYg2G2Btz1KOw4FLAx1wNkvQBABoRNvAYGKnj58baGqNv2Zt74A9/Sjd0osrPfEhRZK9J4beNauZ x7TXchFtXQfgtHQAtpXnXZ4sGV6uZF9sHegduJjYtgNo6krv8+NacmRZpgNtIx3wtA/CjkZY2zvh bO58TPuOtRqnTfp/ll5DOOfC3YEAAAAASUVORK5CYII= "
- id="image4166-8-4"
- x="0"
- y="793.00507"
- style="fill:#4e4b42;fill-opacity:1" />
- </g>
- <g
- style="display:inline;fill:#4e4b42;fill-opacity:1"
- inkscape:label="Trace"
- id="layer2-9-1-52">
- <g
- id="g4216-4-8"
- style="fill:#4e4b42;fill-opacity:1">
- <path
- style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#4e4b42;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:45.99133301;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
- d="M 32.360048,65.606228 A 18.742595,18.742595 0 0 0 13.619017,84.347257 v 52.986143 52.98616 a 18.742595,18.742595 0 0 0 18.741031,18.74102 h 66.931502 52.98614 a 18.742595,18.742595 0 0 0 14.56367,-6.9571 c 9.39466,4.42246 19.71589,6.94877 30.41586,6.9571 a 18.742595,18.742595 0 0 0 0.0133,0 c 39.39268,0.001 71.72926,-32.3342 71.72947,-71.72718 -2.1e-4,-39.392981 -32.33679,-71.728471 -71.72977,-71.727172 a 18.742595,18.742595 0 0 0 -0.0106,0 c -18.56058,0.0092 -35.9395,7.273903 -48.98023,19.491814 C 135.2392,72.880337 117.85926,65.616053 99.29911,65.606228 a 18.742595,18.742595 0 0 0 -0.008,0 z M 32.20937,84.347257 H 99.14088 A 52.987154,52.987154 0 0 1 148.1289,117.2103 52.987154,52.987154 0 0 1 197.12213,84.347257 52.987154,52.987154 0 0 1 250.11087,137.3334 52.987154,52.987154 0 0 1 197.12213,190.31956 52.987154,52.987154 0 0 1 152.12702,165.2527 v 25.06686 H 99.14088 32.20937 V 137.3334 Z"
- id="path4176-3-9"
- inkscape:connector-curvature="0" />
- </g>
- </g>
- </g>
- </g>
-</svg>
diff --git a/src/stylesheets/Paper/Automata/background.svg b/src/stylesheets/Paper/Automata/background.svg
deleted file mode 100644
index 76468b68..00000000
--- a/src/stylesheets/Paper/Automata/background.svg
+++ /dev/null
@@ -1,99 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
- xmlns:dc="http://purl.org/dc/elements/1.1/"
- xmlns:cc="http://creativecommons.org/ns#"
- xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
- xmlns:svg="http://www.w3.org/2000/svg"
- xmlns="http://www.w3.org/2000/svg"
- xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
- xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
- width="7"
- height="7"
- viewBox="0 0 7 7"
- version="1.1"
- id="svg8"
- inkscape:export-filename="D:\Bethesda Game Modding\Mod Organizer 2\stylesheets\Paper\Dark\dots.png"
- inkscape:export-xdpi="307.20001"
- inkscape:export-ydpi="307.20001"
- inkscape:version="0.92.1 r15371"
- sodipodi:docname="background.svg">
- <defs
- id="defs2" />
- <sodipodi:namedview
- id="base"
- pagecolor="#505050"
- bordercolor="#666666"
- borderopacity="1.0"
- inkscape:pageopacity="0"
- inkscape:pageshadow="2"
- inkscape:zoom="63.914764"
- inkscape:cx="9.5445856"
- inkscape:cy="4.6988181"
- inkscape:document-units="px"
- inkscape:current-layer="layer25"
- showgrid="false"
- units="px"
- borderlayer="true"
- inkscape:showpageshadow="false"
- inkscape:snap-page="true"
- inkscape:snap-bbox="true"
- inkscape:bbox-nodes="true"
- inkscape:snap-bbox-edge-midpoints="true"
- inkscape:snap-bbox-midpoints="true"
- showguides="true"
- inkscape:guide-bbox="true"
- inkscape:window-width="1920"
- inkscape:window-height="1027"
- inkscape:window-x="-8"
- inkscape:window-y="-8"
- inkscape:window-maximized="1"
- inkscape:snap-center="true"
- inkscape:snap-object-midpoints="true"
- inkscape:measure-start="-15.1166,28.7091"
- inkscape:measure-end="-12,31"
- inkscape:snap-intersection-paths="true"
- inkscape:object-paths="true"
- inkscape:snap-smooth-nodes="true"
- inkscape:snap-midpoints="true"
- inkscape:snap-global="true"
- inkscape:bbox-paths="true">
- <sodipodi:guide
- position="14,14"
- orientation="0,1"
- id="guide4010"
- inkscape:locked="false" />
- </sodipodi:namedview>
- <metadata
- id="metadata5">
- <rdf:RDF>
- <cc:Work
- rdf:about="">
- <dc:format>image/svg+xml</dc:format>
- <dc:type
- rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
- <dc:title></dc:title>
- </cc:Work>
- </rdf:RDF>
- </metadata>
- <g
- inkscape:groupmode="layer"
- id="layer25"
- inkscape:label="Content"
- transform="translate(0,-13)">
- <path
- sodipodi:nodetypes="cccccc"
- inkscape:connector-curvature="0"
- id="path3876"
- d="M 0,13 V 14.399999 20 h 7 v -7 z"
- style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#c6c1ab;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.99999976;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:stroke fill markers;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
- <rect
- style="opacity:1;fill:#cbc6b0;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:stroke fill markers"
- id="rect4092"
- width="5"
- height="5"
- x="1"
- y="14" />
- </g>
-</svg>
diff --git a/src/stylesheets/Paper/Automata/backup.svg b/src/stylesheets/Paper/Automata/backup.svg
deleted file mode 100644
index 1c55c45f..00000000
--- a/src/stylesheets/Paper/Automata/backup.svg
+++ /dev/null
@@ -1,126 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
- xmlns:dc="http://purl.org/dc/elements/1.1/"
- xmlns:cc="http://creativecommons.org/ns#"
- xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
- xmlns:svg="http://www.w3.org/2000/svg"
- xmlns="http://www.w3.org/2000/svg"
- xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
- xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
- width="20"
- height="20"
- viewBox="0 0 20 20"
- version="1.1"
- id="svg8"
- inkscape:export-filename="D:\Bethesda Game Modding\Mod Organizer 2\stylesheets\Paper\Dark\dots.png"
- inkscape:export-xdpi="307.20001"
- inkscape:export-ydpi="307.20001"
- inkscape:version="0.92.1 r15371"
- sodipodi:docname="backup.svg">
- <defs
- id="defs2" />
- <sodipodi:namedview
- id="base"
- pagecolor="#505050"
- bordercolor="#666666"
- borderopacity="1.0"
- inkscape:pageopacity="0"
- inkscape:pageshadow="2"
- inkscape:zoom="11.29864"
- inkscape:cx="-6.8920047"
- inkscape:cy="14.09106"
- inkscape:document-units="px"
- inkscape:current-layer="layer25"
- showgrid="false"
- units="px"
- borderlayer="true"
- inkscape:showpageshadow="false"
- inkscape:snap-page="true"
- inkscape:snap-bbox="true"
- inkscape:bbox-nodes="true"
- inkscape:snap-bbox-edge-midpoints="true"
- inkscape:snap-bbox-midpoints="true"
- showguides="true"
- inkscape:guide-bbox="true"
- inkscape:window-width="1920"
- inkscape:window-height="1027"
- inkscape:window-x="-8"
- inkscape:window-y="-8"
- inkscape:window-maximized="1"
- inkscape:snap-center="true"
- inkscape:snap-object-midpoints="true"
- inkscape:measure-start="18.8975,12.498"
- inkscape:measure-end="20,12.498"
- inkscape:snap-intersection-paths="true"
- inkscape:object-paths="true"
- inkscape:snap-smooth-nodes="true"
- inkscape:snap-midpoints="true"
- inkscape:snap-global="true">
- <sodipodi:guide
- position="10,10"
- orientation="1,0"
- id="guide4039"
- inkscape:locked="false" />
- <sodipodi:guide
- position="10,10"
- orientation="0,1"
- id="guide3935"
- inkscape:locked="false" />
- </sodipodi:namedview>
- <metadata
- id="metadata5">
- <rdf:RDF>
- <cc:Work
- rdf:about="">
- <dc:format>image/svg+xml</dc:format>
- <dc:type
- rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
- <dc:title />
- </cc:Work>
- </rdf:RDF>
- </metadata>
- <g
- inkscape:groupmode="layer"
- id="layer25"
- inkscape:label="Content">
- <g
- id="g3865"
- style="fill:#4d4a41;fill-opacity:1">
- <path
- id="rect3928"
- d="M 0 0 L 0 3 L 0 6 L 3 6 L 3 3 L 17 3 L 17 6 L 20 6 L 20 3 L 20 0 L 0 0 z "
- style="opacity:1;fill:#4d4a41;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.40999997;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:normal" />
- <g
- id="g3858"
- style="fill:#4d4a41;fill-opacity:1">
- <rect
- y="-12.83"
- x="-20"
- height="5.6599998"
- width="13.981334"
- id="rect3930"
- style="opacity:1;fill:#4d4a41;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.40999997;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:normal"
- transform="matrix(0,-1,-1,0,0,0)" />
- <path
- sodipodi:type="star"
- style="opacity:1;fill:#4d4a41;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.83595276;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:normal"
- id="path3932"
- sodipodi:sides="3"
- sodipodi:cx="8.9146461"
- sodipodi:cy="9.684866"
- sodipodi:r1="4.0292788"
- sodipodi:r2="2.0146394"
- sodipodi:arg1="-1.5707963"
- sodipodi:arg2="-0.52359878"
- inkscape:flatsided="true"
- inkscape:rounded="0"
- inkscape:randomized="0"
- d="m 8.9146463,5.6555872 3.4894577,6.0439178 -6.9789157,0 z"
- inkscape:transform-center-y="-1.3333344"
- transform="matrix(-2.1493311,0,0,1.3236446,29.160526,-4.4859875)" />
- </g>
- </g>
- </g>
-</svg>
diff --git a/src/stylesheets/Paper/Automata/branch.svg b/src/stylesheets/Paper/Automata/branch.svg
deleted file mode 100644
index edabfeab..00000000
--- a/src/stylesheets/Paper/Automata/branch.svg
+++ /dev/null
@@ -1,99 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
- xmlns:dc="http://purl.org/dc/elements/1.1/"
- xmlns:cc="http://creativecommons.org/ns#"
- xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
- xmlns:svg="http://www.w3.org/2000/svg"
- xmlns="http://www.w3.org/2000/svg"
- xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
- xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
- width="20"
- height="20"
- viewBox="0 0 20 20"
- version="1.1"
- id="svg8"
- inkscape:export-filename="D:\Bethesda Game Modding\Mod Organizer 2\stylesheets\Paper\Dark\dots.png"
- inkscape:export-xdpi="307.20001"
- inkscape:export-ydpi="307.20001"
- inkscape:version="0.92.1 r15371"
- sodipodi:docname="branch.svg">
- <defs
- id="defs2" />
- <sodipodi:namedview
- id="base"
- pagecolor="#505050"
- bordercolor="#666666"
- borderopacity="1.0"
- inkscape:pageopacity="0"
- inkscape:pageshadow="2"
- inkscape:zoom="19.449279"
- inkscape:cx="15.344006"
- inkscape:cy="13.455246"
- inkscape:document-units="px"
- inkscape:current-layer="layer25"
- showgrid="false"
- units="px"
- borderlayer="true"
- inkscape:showpageshadow="false"
- inkscape:snap-page="true"
- inkscape:snap-bbox="true"
- inkscape:bbox-nodes="true"
- inkscape:snap-bbox-edge-midpoints="true"
- inkscape:snap-bbox-midpoints="true"
- showguides="true"
- inkscape:guide-bbox="true"
- inkscape:window-width="1920"
- inkscape:window-height="1027"
- inkscape:window-x="-8"
- inkscape:window-y="-8"
- inkscape:window-maximized="1"
- inkscape:snap-center="true"
- inkscape:snap-object-midpoints="true"
- inkscape:measure-start="18.8975,12.498"
- inkscape:measure-end="20,12.498"
- inkscape:snap-intersection-paths="true"
- inkscape:object-paths="true"
- inkscape:snap-smooth-nodes="true"
- inkscape:snap-midpoints="true"
- inkscape:snap-global="true">
- <sodipodi:guide
- position="10,10"
- orientation="1,0"
- id="guide4039"
- inkscape:locked="false" />
- <sodipodi:guide
- position="10,10"
- orientation="0,1"
- id="guide3935"
- inkscape:locked="false" />
- </sodipodi:namedview>
- <metadata
- id="metadata5">
- <rdf:RDF>
- <cc:Work
- rdf:about="">
- <dc:format>image/svg+xml</dc:format>
- <dc:type
- rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
- <dc:title />
- </cc:Work>
- </rdf:RDF>
- </metadata>
- <g
- inkscape:groupmode="layer"
- id="layer25"
- inkscape:label="Content">
- <g
- id="g3867"
- transform="matrix(2,0,0,2,-11.124285,-10)"
- style="stroke-width:0.5;fill:#b4af9a;fill-opacity:1">
- <path
- style="opacity:1;fill:#b4af9a;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:stroke fill markers"
- d="M 4 0 L 4 20 L 10 20 L 10 0 L 4 0 z M 14 0 L 14 20 L 16 20 L 16 0 L 14 0 z "
- transform="matrix(0.5,0,0,0.5,5.5621425,5)"
- id="rect3861" />
- </g>
- </g>
-</svg>
diff --git a/src/stylesheets/Paper/Automata/collapsed.svg b/src/stylesheets/Paper/Automata/collapsed.svg
deleted file mode 100644
index 614cca6b..00000000
--- a/src/stylesheets/Paper/Automata/collapsed.svg
+++ /dev/null
@@ -1,97 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
- xmlns:dc="http://purl.org/dc/elements/1.1/"
- xmlns:cc="http://creativecommons.org/ns#"
- xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
- xmlns:svg="http://www.w3.org/2000/svg"
- xmlns="http://www.w3.org/2000/svg"
- xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
- xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
- width="14"
- height="14"
- viewBox="0 0 14 14"
- version="1.1"
- id="svg8"
- inkscape:export-filename="D:\Bethesda Game Modding\Mod Organizer 2\stylesheets\Paper\Dark\dots.png"
- inkscape:export-xdpi="307.20001"
- inkscape:export-ydpi="307.20001"
- inkscape:version="0.92.1 r15371"
- sodipodi:docname="collapsed.svg">
- <defs
- id="defs2" />
- <sodipodi:namedview
- id="base"
- pagecolor="#505050"
- bordercolor="#666666"
- borderopacity="1.0"
- inkscape:pageopacity="0"
- inkscape:pageshadow="2"
- inkscape:zoom="26.733251"
- inkscape:cx="5.8544957"
- inkscape:cy="4.1342917"
- inkscape:document-units="px"
- inkscape:current-layer="layer25"
- showgrid="false"
- units="px"
- borderlayer="true"
- inkscape:showpageshadow="false"
- inkscape:snap-page="true"
- inkscape:snap-bbox="true"
- inkscape:bbox-nodes="true"
- inkscape:snap-bbox-edge-midpoints="true"
- inkscape:snap-bbox-midpoints="true"
- showguides="false"
- inkscape:guide-bbox="true"
- inkscape:window-width="1920"
- inkscape:window-height="1027"
- inkscape:window-x="-8"
- inkscape:window-y="-8"
- inkscape:window-maximized="1"
- inkscape:snap-center="true"
- inkscape:snap-object-midpoints="true"
- inkscape:measure-start="2,12"
- inkscape:measure-end="0,12"
- inkscape:snap-intersection-paths="true"
- inkscape:object-paths="true"
- inkscape:snap-smooth-nodes="true"
- inkscape:snap-midpoints="true"
- inkscape:snap-global="true">
- <sodipodi:guide
- position="7,7"
- orientation="1,0"
- id="guide4039"
- inkscape:locked="false" />
- <sodipodi:guide
- position="7,7"
- orientation="0,1"
- id="guide3935"
- inkscape:locked="false"
- inkscape:label=""
- inkscape:color="rgb(0,0,255)" />
- </sodipodi:namedview>
- <metadata
- id="metadata5">
- <rdf:RDF>
- <cc:Work
- rdf:about="">
- <dc:format>image/svg+xml</dc:format>
- <dc:type
- rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
- <dc:title></dc:title>
- </cc:Work>
- </rdf:RDF>
- </metadata>
- <g
- inkscape:groupmode="layer"
- id="layer25"
- inkscape:label="Content"
- transform="translate(0,-6)">
- <path
- style="opacity:1;fill:#4d4a41;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:stroke fill markers"
- d="M 0 0 L 0 14 L 14 14 L 14 0 L 0 0 z M 6 4 L 8 4 L 8 6 L 10 6 L 10 8 L 8 8 L 8 10 L 6 10 L 6 8 L 4 8 L 4 6 L 6 6 L 6 4 z "
- transform="translate(0,6)"
- id="rect3848" />
- </g>
-</svg>
diff --git a/src/stylesheets/Paper/Automata/cross.svg b/src/stylesheets/Paper/Automata/cross.svg
deleted file mode 100644
index 450623c3..00000000
--- a/src/stylesheets/Paper/Automata/cross.svg
+++ /dev/null
@@ -1,94 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
- xmlns:dc="http://purl.org/dc/elements/1.1/"
- xmlns:cc="http://creativecommons.org/ns#"
- xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
- xmlns:svg="http://www.w3.org/2000/svg"
- xmlns="http://www.w3.org/2000/svg"
- xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
- xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
- width="20"
- height="20"
- viewBox="0 0 20 20"
- version="1.1"
- id="svg8"
- inkscape:export-filename="D:\Bethesda Game Modding\Mod Organizer 2\stylesheets\Paper\Dark\dots.png"
- inkscape:export-xdpi="307.20001"
- inkscape:export-ydpi="307.20001"
- inkscape:version="0.92.1 r15371"
- sodipodi:docname="cross.svg">
- <defs
- id="defs2" />
- <sodipodi:namedview
- id="base"
- pagecolor="#505050"
- bordercolor="#666666"
- borderopacity="1.0"
- inkscape:pageopacity="0"
- inkscape:pageshadow="2"
- inkscape:zoom="46.5"
- inkscape:cx="10"
- inkscape:cy="10"
- inkscape:document-units="px"
- inkscape:current-layer="layer25"
- showgrid="false"
- units="px"
- borderlayer="true"
- inkscape:showpageshadow="false"
- inkscape:snap-page="true"
- inkscape:snap-bbox="true"
- inkscape:bbox-nodes="true"
- inkscape:snap-bbox-edge-midpoints="true"
- inkscape:snap-bbox-midpoints="true"
- showguides="true"
- inkscape:guide-bbox="true"
- inkscape:window-width="1920"
- inkscape:window-height="1027"
- inkscape:window-x="-8"
- inkscape:window-y="-8"
- inkscape:window-maximized="1"
- inkscape:snap-center="true"
- inkscape:snap-object-midpoints="true"
- inkscape:measure-start="18.8975,12.498"
- inkscape:measure-end="20,12.498"
- inkscape:snap-intersection-paths="true"
- inkscape:object-paths="true"
- inkscape:snap-smooth-nodes="true"
- inkscape:snap-midpoints="true"
- inkscape:snap-global="true">
- <sodipodi:guide
- position="10,10"
- orientation="1,0"
- id="guide4039"
- inkscape:locked="false" />
- <sodipodi:guide
- position="10,10"
- orientation="0,1"
- id="guide3935"
- inkscape:locked="false" />
- </sodipodi:namedview>
- <metadata
- id="metadata5">
- <rdf:RDF>
- <cc:Work
- rdf:about="">
- <dc:format>image/svg+xml</dc:format>
- <dc:type
- rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
- <dc:title />
- </cc:Work>
- </rdf:RDF>
- </metadata>
- <g
- inkscape:groupmode="layer"
- id="layer25"
- inkscape:label="Content">
- <path
- style="display:inline;opacity:1;fill:#4d4a41;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:169.70561218;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:1.93939996;stroke-opacity:1;paint-order:markers stroke fill"
- d="m 4,1.5e-7 -4,3.99999995 2,2 L 6,10 l -4,4 -2,2 4,4 2,-2 4,-4 4,4 2,2 4,-4 -2,-2 -4,-4 4,-3.9999999 2,-2 L 16,1.5e-7 14,2.0000002 10,6.0000001 6,2.0000002 Z"
- id="path3944"
- inkscape:connector-curvature="0" />
- </g>
-</svg>
diff --git a/src/stylesheets/Paper/Automata/dots.svg b/src/stylesheets/Paper/Automata/dots.svg
deleted file mode 100644
index 08d5139f..00000000
--- a/src/stylesheets/Paper/Automata/dots.svg
+++ /dev/null
@@ -1,111 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
- xmlns:dc="http://purl.org/dc/elements/1.1/"
- xmlns:cc="http://creativecommons.org/ns#"
- xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
- xmlns:svg="http://www.w3.org/2000/svg"
- xmlns="http://www.w3.org/2000/svg"
- xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
- xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
- width="20"
- height="20"
- viewBox="0 0 20 20"
- version="1.1"
- id="svg8"
- inkscape:export-filename="D:\Bethesda Game Modding\Mod Organizer 2\stylesheets\Paper\Dark\dots.png"
- inkscape:export-xdpi="307.20001"
- inkscape:export-ydpi="307.20001"
- inkscape:version="0.92.1 r15371"
- sodipodi:docname="dots.svg">
- <defs
- id="defs2" />
- <sodipodi:namedview
- id="base"
- pagecolor="#505050"
- bordercolor="#666666"
- borderopacity="1.0"
- inkscape:pageopacity="0"
- inkscape:pageshadow="2"
- inkscape:zoom="46.5"
- inkscape:cx="10"
- inkscape:cy="10"
- inkscape:document-units="px"
- inkscape:current-layer="layer25"
- showgrid="false"
- units="px"
- borderlayer="true"
- inkscape:showpageshadow="false"
- inkscape:snap-page="true"
- inkscape:snap-bbox="true"
- inkscape:bbox-nodes="true"
- inkscape:snap-bbox-edge-midpoints="true"
- inkscape:snap-bbox-midpoints="true"
- showguides="true"
- inkscape:guide-bbox="true"
- inkscape:window-width="1920"
- inkscape:window-height="1027"
- inkscape:window-x="-8"
- inkscape:window-y="-8"
- inkscape:window-maximized="1"
- inkscape:snap-center="true"
- inkscape:snap-object-midpoints="true"
- inkscape:measure-start="18.8975,12.498"
- inkscape:measure-end="20,12.498"
- inkscape:snap-intersection-paths="true"
- inkscape:object-paths="true"
- inkscape:snap-smooth-nodes="true"
- inkscape:snap-midpoints="true"
- inkscape:snap-global="true">
- <sodipodi:guide
- position="10,10"
- orientation="1,0"
- id="guide4039"
- inkscape:locked="false" />
- <sodipodi:guide
- position="10,10"
- orientation="0,1"
- id="guide3935"
- inkscape:locked="false" />
- </sodipodi:namedview>
- <metadata
- id="metadata5">
- <rdf:RDF>
- <cc:Work
- rdf:about="">
- <dc:format>image/svg+xml</dc:format>
- <dc:type
- rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
- <dc:title />
- </cc:Work>
- </rdf:RDF>
- </metadata>
- <g
- inkscape:groupmode="layer"
- id="layer25"
- inkscape:label="Content">
- <g
- id="g3853"
- style="fill:#4d4a41;fill-opacity:1">
- <circle
- r="2.5"
- cy="10"
- cx="10"
- id="path4063"
- style="opacity:1;fill:#4d4a41;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:3.99999952;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:stroke fill markers" />
- <circle
- style="opacity:1;fill:#4d4a41;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:3.99999952;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:stroke fill markers"
- id="circle3937"
- cx="17.5"
- cy="10"
- r="2.5" />
- <circle
- r="2.5"
- cy="10"
- cx="2.5"
- id="circle3939"
- style="opacity:1;fill:#4d4a41;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:3.99999952;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:stroke fill markers" />
- </g>
- </g>
-</svg>
diff --git a/src/stylesheets/Paper/Automata/expanded.svg b/src/stylesheets/Paper/Automata/expanded.svg
deleted file mode 100644
index 19f776d3..00000000
--- a/src/stylesheets/Paper/Automata/expanded.svg
+++ /dev/null
@@ -1,104 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
- xmlns:dc="http://purl.org/dc/elements/1.1/"
- xmlns:cc="http://creativecommons.org/ns#"
- xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
- xmlns:svg="http://www.w3.org/2000/svg"
- xmlns="http://www.w3.org/2000/svg"
- xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
- xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
- width="14"
- height="14"
- viewBox="0 0 14 14"
- version="1.1"
- id="svg8"
- inkscape:export-filename="D:\Bethesda Game Modding\Mod Organizer 2\stylesheets\Paper\Dark\dots.png"
- inkscape:export-xdpi="307.20001"
- inkscape:export-ydpi="307.20001"
- inkscape:version="0.92.1 r15371"
- sodipodi:docname="expanded.svg">
- <defs
- id="defs2" />
- <sodipodi:namedview
- id="base"
- pagecolor="#505050"
- bordercolor="#666666"
- borderopacity="1.0"
- inkscape:pageopacity="0"
- inkscape:pageshadow="2"
- inkscape:zoom="14.407815"
- inkscape:cx="5.5919938"
- inkscape:cy="2.0247774"
- inkscape:document-units="px"
- inkscape:current-layer="layer25"
- showgrid="false"
- units="px"
- borderlayer="true"
- inkscape:showpageshadow="false"
- inkscape:snap-page="true"
- inkscape:snap-bbox="true"
- inkscape:bbox-nodes="true"
- inkscape:snap-bbox-edge-midpoints="true"
- inkscape:snap-bbox-midpoints="true"
- showguides="false"
- inkscape:guide-bbox="true"
- inkscape:window-width="1920"
- inkscape:window-height="1027"
- inkscape:window-x="-8"
- inkscape:window-y="-8"
- inkscape:window-maximized="1"
- inkscape:snap-center="true"
- inkscape:snap-object-midpoints="true"
- inkscape:measure-start="2,12"
- inkscape:measure-end="0,12"
- inkscape:snap-intersection-paths="true"
- inkscape:object-paths="true"
- inkscape:snap-smooth-nodes="true"
- inkscape:snap-midpoints="true"
- inkscape:snap-global="true">
- <sodipodi:guide
- position="7,7"
- orientation="1,0"
- id="guide4039"
- inkscape:locked="false" />
- <sodipodi:guide
- position="7,7"
- orientation="0,1"
- id="guide3935"
- inkscape:locked="false"
- inkscape:label=""
- inkscape:color="rgb(0,0,255)" />
- </sodipodi:namedview>
- <metadata
- id="metadata5">
- <rdf:RDF>
- <cc:Work
- rdf:about="">
- <dc:format>image/svg+xml</dc:format>
- <dc:type
- rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
- <dc:title></dc:title>
- </cc:Work>
- </rdf:RDF>
- </metadata>
- <g
- inkscape:groupmode="layer"
- id="layer25"
- inkscape:label="Content"
- transform="translate(0,-6)">
- <path
- style="opacity:1;fill:#4d4a41;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:stroke fill markers"
- d="M 0 0 L 0 14 L 14 14 L 14 0 L 0 0 z M 2 2 L 12 2 L 12 12 L 2 12 L 2 2 z "
- transform="translate(0,6)"
- id="rect3848" />
- <rect
- style="opacity:1;fill:#4d4a41;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:3.99999976;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:stroke fill markers"
- id="rect3849"
- width="6"
- height="2"
- x="4"
- y="12" />
- </g>
-</svg>
diff --git a/src/stylesheets/Paper/Automata/folder.svg b/src/stylesheets/Paper/Automata/folder.svg
deleted file mode 100644
index 4ac83f47..00000000
--- a/src/stylesheets/Paper/Automata/folder.svg
+++ /dev/null
@@ -1,94 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
- xmlns:dc="http://purl.org/dc/elements/1.1/"
- xmlns:cc="http://creativecommons.org/ns#"
- xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
- xmlns:svg="http://www.w3.org/2000/svg"
- xmlns="http://www.w3.org/2000/svg"
- xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
- xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
- width="20"
- height="20"
- viewBox="0 0 20 20"
- version="1.1"
- id="svg8"
- inkscape:export-filename="D:\Bethesda Game Modding\Mod Organizer 2\stylesheets\Paper\Dark\dots.png"
- inkscape:export-xdpi="307.20001"
- inkscape:export-ydpi="307.20001"
- inkscape:version="0.92.1 r15371"
- sodipodi:docname="folder.svg">
- <defs
- id="defs2" />
- <sodipodi:namedview
- id="base"
- pagecolor="#505050"
- bordercolor="#666666"
- borderopacity="1.0"
- inkscape:pageopacity="0"
- inkscape:pageshadow="2"
- inkscape:zoom="46.5"
- inkscape:cx="10"
- inkscape:cy="10"
- inkscape:document-units="px"
- inkscape:current-layer="layer25"
- showgrid="false"
- units="px"
- borderlayer="true"
- inkscape:showpageshadow="false"
- inkscape:snap-page="true"
- inkscape:snap-bbox="true"
- inkscape:bbox-nodes="true"
- inkscape:snap-bbox-edge-midpoints="true"
- inkscape:snap-bbox-midpoints="true"
- showguides="true"
- inkscape:guide-bbox="true"
- inkscape:window-width="1920"
- inkscape:window-height="1027"
- inkscape:window-x="-8"
- inkscape:window-y="-8"
- inkscape:window-maximized="1"
- inkscape:snap-center="true"
- inkscape:snap-object-midpoints="true"
- inkscape:measure-start="18.8975,12.498"
- inkscape:measure-end="20,12.498"
- inkscape:snap-intersection-paths="true"
- inkscape:object-paths="true"
- inkscape:snap-smooth-nodes="true"
- inkscape:snap-midpoints="true"
- inkscape:snap-global="true">
- <sodipodi:guide
- position="10,10"
- orientation="1,0"
- id="guide4039"
- inkscape:locked="false" />
- <sodipodi:guide
- position="10,10"
- orientation="0,1"
- id="guide3935"
- inkscape:locked="false" />
- </sodipodi:namedview>
- <metadata
- id="metadata5">
- <rdf:RDF>
- <cc:Work
- rdf:about="">
- <dc:format>image/svg+xml</dc:format>
- <dc:type
- rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
- <dc:title />
- </cc:Work>
- </rdf:RDF>
- </metadata>
- <g
- inkscape:groupmode="layer"
- id="layer25"
- inkscape:label="Content">
- <path
- style="display:inline;opacity:1;fill:#4d4a41;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:normal"
- d="M 12,2.0000002 10,4.0000001 H 2.5e-7 V 18 H 20 V 4.0000001 L 18,2.0000002 h -3 z"
- id="rect3909"
- inkscape:connector-curvature="0" />
- </g>
-</svg>
diff --git a/src/stylesheets/Paper/Automata/heart.svg b/src/stylesheets/Paper/Automata/heart.svg
deleted file mode 100644
index 396fab55..00000000
--- a/src/stylesheets/Paper/Automata/heart.svg
+++ /dev/null
@@ -1,111 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
- xmlns:dc="http://purl.org/dc/elements/1.1/"
- xmlns:cc="http://creativecommons.org/ns#"
- xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
- xmlns:svg="http://www.w3.org/2000/svg"
- xmlns="http://www.w3.org/2000/svg"
- xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
- xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
- width="20"
- height="20"
- viewBox="0 0 20 20"
- version="1.1"
- id="svg8"
- inkscape:export-filename="D:\Bethesda Game Modding\Mod Organizer 2\stylesheets\Paper\Dark\dots.png"
- inkscape:export-xdpi="307.20001"
- inkscape:export-ydpi="307.20001"
- inkscape:version="0.92.1 r15371"
- sodipodi:docname="heart.svg">
- <defs
- id="defs2" />
- <sodipodi:namedview
- id="base"
- pagecolor="#505050"
- bordercolor="#666666"
- borderopacity="1.0"
- inkscape:pageopacity="0"
- inkscape:pageshadow="2"
- inkscape:zoom="46.5"
- inkscape:cx="10"
- inkscape:cy="10"
- inkscape:document-units="px"
- inkscape:current-layer="layer25"
- showgrid="false"
- units="px"
- borderlayer="true"
- inkscape:showpageshadow="false"
- inkscape:snap-page="true"
- inkscape:snap-bbox="true"
- inkscape:bbox-nodes="true"
- inkscape:snap-bbox-edge-midpoints="true"
- inkscape:snap-bbox-midpoints="true"
- showguides="true"
- inkscape:guide-bbox="true"
- inkscape:window-width="1920"
- inkscape:window-height="1027"
- inkscape:window-x="-8"
- inkscape:window-y="-8"
- inkscape:window-maximized="1"
- inkscape:snap-center="true"
- inkscape:snap-object-midpoints="true"
- inkscape:measure-start="18.8975,12.498"
- inkscape:measure-end="20,12.498"
- inkscape:snap-intersection-paths="true"
- inkscape:object-paths="true"
- inkscape:snap-smooth-nodes="true"
- inkscape:snap-midpoints="true"
- inkscape:snap-global="true">
- <sodipodi:guide
- position="10,10"
- orientation="1,0"
- id="guide4039"
- inkscape:locked="false" />
- <sodipodi:guide
- position="10,10"
- orientation="0,1"
- id="guide3935"
- inkscape:locked="false" />
- </sodipodi:namedview>
- <metadata
- id="metadata5">
- <rdf:RDF>
- <cc:Work
- rdf:about="">
- <dc:format>image/svg+xml</dc:format>
- <dc:type
- rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
- <dc:title />
- </cc:Work>
- </rdf:RDF>
- </metadata>
- <g
- inkscape:groupmode="layer"
- id="layer25"
- inkscape:label="Content">
- <g
- style="display:inline;fill:#4d4a41;fill-opacity:1"
- id="g4198"
- transform="translate(0,1)">
- <circle
- r="5.5"
- cy="5.5"
- cx="5.5"
- id="path4174"
- style="opacity:1;fill:#4d4a41;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:5.99999952;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:normal" />
- <circle
- r="5.5"
- style="opacity:1;fill:#4d4a41;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:6;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:normal"
- id="circle4176"
- cx="14.5"
- cy="5.5" />
- <path
- id="path4178"
- d="M 0,5.5 C 0,10.950465 5.8158604,15.070234 10,18 14.18414,15.070234 20,10.950465 20,5.5 H 10 Z"
- style="fill:#4d4a41;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.99999994px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
- inkscape:connector-curvature="0" />
- </g>
- </g>
-</svg>
diff --git a/src/stylesheets/Paper/Automata/refresh.svg b/src/stylesheets/Paper/Automata/refresh.svg
deleted file mode 100644
index a148aa1d..00000000
--- a/src/stylesheets/Paper/Automata/refresh.svg
+++ /dev/null
@@ -1,103 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
- xmlns:dc="http://purl.org/dc/elements/1.1/"
- xmlns:cc="http://creativecommons.org/ns#"
- xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
- xmlns:svg="http://www.w3.org/2000/svg"
- xmlns="http://www.w3.org/2000/svg"
- xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
- xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
- width="20"
- height="20"
- viewBox="0 0 20 20"
- version="1.1"
- id="svg8"
- inkscape:export-filename="D:\Bethesda Game Modding\Mod Organizer 2\stylesheets\Paper\Dark\dots.png"
- inkscape:export-xdpi="307.20001"
- inkscape:export-ydpi="307.20001"
- inkscape:version="0.92.1 r15371"
- sodipodi:docname="refresh.svg">
- <defs
- id="defs2" />
- <sodipodi:namedview
- id="base"
- pagecolor="#505050"
- bordercolor="#666666"
- borderopacity="1.0"
- inkscape:pageopacity="0"
- inkscape:pageshadow="2"
- inkscape:zoom="46.5"
- inkscape:cx="10"
- inkscape:cy="10"
- inkscape:document-units="px"
- inkscape:current-layer="layer25"
- showgrid="false"
- units="px"
- borderlayer="true"
- inkscape:showpageshadow="false"
- inkscape:snap-page="true"
- inkscape:snap-bbox="true"
- inkscape:bbox-nodes="true"
- inkscape:snap-bbox-edge-midpoints="true"
- inkscape:snap-bbox-midpoints="true"
- showguides="true"
- inkscape:guide-bbox="true"
- inkscape:window-width="1920"
- inkscape:window-height="1027"
- inkscape:window-x="-8"
- inkscape:window-y="-8"
- inkscape:window-maximized="1"
- inkscape:snap-center="true"
- inkscape:snap-object-midpoints="true"
- inkscape:measure-start="18.8975,12.498"
- inkscape:measure-end="20,12.498"
- inkscape:snap-intersection-paths="true"
- inkscape:object-paths="true"
- inkscape:snap-smooth-nodes="true"
- inkscape:snap-midpoints="true"
- inkscape:snap-global="true">
- <sodipodi:guide
- position="10,10"
- orientation="1,0"
- id="guide4039"
- inkscape:locked="false" />
- <sodipodi:guide
- position="10,10"
- orientation="0,1"
- id="guide3935"
- inkscape:locked="false" />
- </sodipodi:namedview>
- <metadata
- id="metadata5">
- <rdf:RDF>
- <cc:Work
- rdf:about="">
- <dc:format>image/svg+xml</dc:format>
- <dc:type
- rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
- <dc:title />
- </cc:Work>
- </rdf:RDF>
- </metadata>
- <g
- inkscape:groupmode="layer"
- id="layer25"
- inkscape:label="Content">
- <g
- style="display:inline;fill:#4d4a41;fill-opacity:1"
- id="g3986">
- <path
- inkscape:connector-curvature="0"
- id="path4137"
- d="M 0,11.998047 V 20 L 2.9335938,17.066406 A 10,10 0 0 0 10,20 10,10 0 0 0 19.787109,11.998047 H 16.705078 A 7,7 0 0 1 10,17 7,7 0 0 1 5.0527344,14.947266 L 8,11.998047 Z"
- style="opacity:1;fill:#4d4a41;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:6;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:normal" />
- <path
- inkscape:connector-curvature="0"
- id="rect4142"
- d="M 10,0 A 10,10 0 0 0 0.21289062,8.0019531 H 3.2949219 A 7,7 0 0 1 10,3 7,7 0 0 1 14.947266,5.0527344 L 12,8.0019531 h 8 V 0 L 17.066406,2.9335938 A 10,10 0 0 0 10,0 Z"
- style="opacity:1;fill:#4d4a41;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:6;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:normal" />
- </g>
- </g>
-</svg>
diff --git a/src/stylesheets/Paper/Automata/restore-alt.svg b/src/stylesheets/Paper/Automata/restore-alt.svg
deleted file mode 100644
index 79f96c1a..00000000
--- a/src/stylesheets/Paper/Automata/restore-alt.svg
+++ /dev/null
@@ -1,126 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
- xmlns:dc="http://purl.org/dc/elements/1.1/"
- xmlns:cc="http://creativecommons.org/ns#"
- xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
- xmlns:svg="http://www.w3.org/2000/svg"
- xmlns="http://www.w3.org/2000/svg"
- xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
- xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
- width="20"
- height="20"
- viewBox="0 0 20 20"
- version="1.1"
- id="svg8"
- inkscape:export-filename="D:\Bethesda Game Modding\Mod Organizer 2\stylesheets\Paper\Dark\dots.png"
- inkscape:export-xdpi="307.20001"
- inkscape:export-ydpi="307.20001"
- inkscape:version="0.92.1 r15371"
- sodipodi:docname="restore-alt.svg">
- <defs
- id="defs2" />
- <sodipodi:namedview
- id="base"
- pagecolor="#505050"
- bordercolor="#666666"
- borderopacity="1.0"
- inkscape:pageopacity="0"
- inkscape:pageshadow="2"
- inkscape:zoom="90.389123"
- inkscape:cx="14.98971"
- inkscape:cy="2.6868089"
- inkscape:document-units="px"
- inkscape:current-layer="layer25"
- showgrid="false"
- units="px"
- borderlayer="true"
- inkscape:showpageshadow="false"
- inkscape:snap-page="true"
- inkscape:snap-bbox="true"
- inkscape:bbox-nodes="true"
- inkscape:snap-bbox-edge-midpoints="true"
- inkscape:snap-bbox-midpoints="true"
- showguides="true"
- inkscape:guide-bbox="true"
- inkscape:window-width="1920"
- inkscape:window-height="1027"
- inkscape:window-x="-8"
- inkscape:window-y="-8"
- inkscape:window-maximized="1"
- inkscape:snap-center="true"
- inkscape:snap-object-midpoints="true"
- inkscape:measure-start="18.8975,12.498"
- inkscape:measure-end="20,12.498"
- inkscape:snap-intersection-paths="true"
- inkscape:object-paths="true"
- inkscape:snap-smooth-nodes="true"
- inkscape:snap-midpoints="true"
- inkscape:snap-global="true">
- <sodipodi:guide
- position="10,10"
- orientation="1,0"
- id="guide4039"
- inkscape:locked="false" />
- <sodipodi:guide
- position="10,10"
- orientation="0,1"
- id="guide3935"
- inkscape:locked="false" />
- </sodipodi:namedview>
- <metadata
- id="metadata5">
- <rdf:RDF>
- <cc:Work
- rdf:about="">
- <dc:format>image/svg+xml</dc:format>
- <dc:type
- rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
- <dc:title />
- </cc:Work>
- </rdf:RDF>
- </metadata>
- <g
- inkscape:groupmode="layer"
- id="layer25"
- inkscape:label="Content">
- <g
- id="g3866"
- style="fill:#4d4a41;fill-opacity:1;stroke:none">
- <path
- id="rect3928"
- d="M 0 14 L 0 17 L 0 20 L 20 20 L 20 17 L 20 14 L 17 14 L 17 17 L 3 17 L 3 14 L 0 14 z "
- style="opacity:1;fill:#4d4a41;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.40999997;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:normal" />
- <g
- id="g3860"
- style="fill:#4d4a41;fill-opacity:1;stroke:none">
- <rect
- y="-12.83"
- x="7.85585e-016"
- height="5.6599998"
- width="13.981334"
- id="rect3930"
- style="opacity:1;fill:#4d4a41;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.40999997;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:normal"
- transform="rotate(90)" />
- <path
- sodipodi:type="star"
- style="opacity:1;fill:#4d4a41;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.83595276;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:normal"
- id="path3932"
- sodipodi:sides="3"
- sodipodi:cx="8.9146461"
- sodipodi:cy="9.684866"
- sodipodi:r1="4.0292788"
- sodipodi:r2="2.0146394"
- sodipodi:arg1="-1.5707963"
- sodipodi:arg2="-0.52359878"
- inkscape:flatsided="true"
- inkscape:rounded="0"
- inkscape:randomized="0"
- d="m 8.9146463,5.6555872 3.4894577,6.0439178 -6.9789157,0 z"
- inkscape:transform-center-y="-1.3333344"
- transform="matrix(-2.1493311,0,0,-1.3236446,29.160526,24.485987)" />
- </g>
- </g>
- </g>
-</svg>
diff --git a/src/stylesheets/Paper/Automata/restore.svg b/src/stylesheets/Paper/Automata/restore.svg
deleted file mode 100644
index b5fbbee1..00000000
--- a/src/stylesheets/Paper/Automata/restore.svg
+++ /dev/null
@@ -1,94 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
- xmlns:dc="http://purl.org/dc/elements/1.1/"
- xmlns:cc="http://creativecommons.org/ns#"
- xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
- xmlns:svg="http://www.w3.org/2000/svg"
- xmlns="http://www.w3.org/2000/svg"
- xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
- xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
- width="20"
- height="20"
- viewBox="0 0 20 20"
- version="1.1"
- id="svg8"
- inkscape:export-filename="D:\Bethesda Game Modding\Mod Organizer 2\stylesheets\Paper\Dark\dots.png"
- inkscape:export-xdpi="307.20001"
- inkscape:export-ydpi="307.20001"
- inkscape:version="0.92.1 r15371"
- sodipodi:docname="restore.svg">
- <defs
- id="defs2" />
- <sodipodi:namedview
- id="base"
- pagecolor="#505050"
- bordercolor="#666666"
- borderopacity="1.0"
- inkscape:pageopacity="0"
- inkscape:pageshadow="2"
- inkscape:zoom="46.5"
- inkscape:cx="10"
- inkscape:cy="10"
- inkscape:document-units="px"
- inkscape:current-layer="layer25"
- showgrid="false"
- units="px"
- borderlayer="true"
- inkscape:showpageshadow="false"
- inkscape:snap-page="true"
- inkscape:snap-bbox="true"
- inkscape:bbox-nodes="true"
- inkscape:snap-bbox-edge-midpoints="true"
- inkscape:snap-bbox-midpoints="true"
- showguides="true"
- inkscape:guide-bbox="true"
- inkscape:window-width="1920"
- inkscape:window-height="1027"
- inkscape:window-x="-8"
- inkscape:window-y="-8"
- inkscape:window-maximized="1"
- inkscape:snap-center="true"
- inkscape:snap-object-midpoints="true"
- inkscape:measure-start="18.8975,12.498"
- inkscape:measure-end="20,12.498"
- inkscape:snap-intersection-paths="true"
- inkscape:object-paths="true"
- inkscape:snap-smooth-nodes="true"
- inkscape:snap-midpoints="true"
- inkscape:snap-global="true">
- <sodipodi:guide
- position="10,10"
- orientation="1,0"
- id="guide4039"
- inkscape:locked="false" />
- <sodipodi:guide
- position="10,10"
- orientation="0,1"
- id="guide3935"
- inkscape:locked="false" />
- </sodipodi:namedview>
- <metadata
- id="metadata5">
- <rdf:RDF>
- <cc:Work
- rdf:about="">
- <dc:format>image/svg+xml</dc:format>
- <dc:type
- rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
- <dc:title />
- </cc:Work>
- </rdf:RDF>
- </metadata>
- <g
- inkscape:groupmode="layer"
- id="layer25"
- inkscape:label="Content">
- <path
- style="opacity:1;fill:#4d4a41;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:3.00000024;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:stroke fill markers"
- d="m 10,-2.5e-7 a 10,10 0 0 1 0.998047,0.05078 A 10,10 0 0 1 19.787109,8.0019518 10,10 0 0 1 20,10 10,10 0 0 1 10,20 10,10 0 0 1 0.2128895,11.998047 h 3.082032 A 7,7 0 0 0 10,17 a 7,7 0 0 0 7,-7 7,7 0 0 0 -0.294922,-1.9980473 7,7 0 0 0 -0.53125,-1.291016 7,7 0 0 0 0,-0.0019 7,7 0 0 0 -0.359375,-0.599609 7,7 0 0 0 -0.417969,-0.5625 7,7 0 0 0 -0.40625,-0.447266 7,7 0 0 0 -0.07422,-0.07813 7,7 0 0 0 -0.435547,-0.390625 7,7 0 0 0 -0.08594,-0.07422 7,7 0 0 0 -0.490234,-0.359375 7,7 0 0 0 -0.07227,-0.05078 7,7 0 0 0 -0.49023,-0.289062 7,7 0 0 0 -0.119141,-0.06836 7,7 0 0 0 -0.439453,-0.203125 7,7 0 0 0 -0.207031,-0.0918 7,7 0 0 0 -0.402344,-0.138672 7,7 0 0 0 -0.259766,-0.08594 7,7 0 0 0 -0.466797,-0.109375 7,7 0 0 0 -0.208984,-0.04687 7,7 0 0 0 -0.458984,-0.06055 7,7 0 0 0 -0.246094,-0.03125 A 7,7 0 0 0 10,2.9999997 a 7,7 0 0 0 -2.06836,0.314453 7,7 0 0 0 -0.0019,0 7,7 0 0 0 -0.654297,0.240234 7,7 0 0 0 -0.0019,0 7,7 0 0 0 -0.628906,0.304688 7,7 0 0 0 -1.587891,1.197266 L 8,8.0019527 H 3.2968745 0.2031245 -5e-7 V -2.5e-7 l 2.933594,2.93359395 a 10,10 0 0 1 0.01172,-0.01172 10,10 0 0 1 0.591797,-0.535156 10,10 0 0 1 0.169922,-0.148438 10,10 0 0 1 0.6015635,-0.447265 10,10 0 0 1 0.199218,-0.142579 10,10 0 0 1 0.644532,-0.380859 10,10 0 0 1 0.222656,-0.126953 10,10 0 0 1 0.632812,-0.29492195 10,10 0 0 1 0.283204,-0.126953 10,10 0 0 1 0.623046,-0.216797 10,10 0 0 1 0.328125,-0.109375 10,10 0 0 1 0.601563,-0.144531 10,10 0 0 1 0.375,-0.08398 10,10 0 0 1 0.603516,-0.08203 10,10 0 0 1 0.394531,-0.04883 A 10,10 0 0 1 10,-2.5e-7 Z"
- id="path3956"
- inkscape:connector-curvature="0" />
- </g>
-</svg>
diff --git a/src/stylesheets/Paper/Automata/run.svg b/src/stylesheets/Paper/Automata/run.svg
deleted file mode 100644
index e969e49f..00000000
--- a/src/stylesheets/Paper/Automata/run.svg
+++ /dev/null
@@ -1,109 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
- xmlns:dc="http://purl.org/dc/elements/1.1/"
- xmlns:cc="http://creativecommons.org/ns#"
- xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
- xmlns:svg="http://www.w3.org/2000/svg"
- xmlns="http://www.w3.org/2000/svg"
- xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
- xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
- width="128"
- height="128"
- viewBox="0 0 128 128"
- version="1.1"
- id="svg8"
- inkscape:export-filename="D:\Bethesda Game Modding\Mod Organizer 2\stylesheets\Paper\Dark\dots.png"
- inkscape:export-xdpi="307.20001"
- inkscape:export-ydpi="307.20001"
- inkscape:version="0.92.1 r15371"
- sodipodi:docname="run.svg">
- <defs
- id="defs2" />
- <sodipodi:namedview
- id="base"
- pagecolor="#505050"
- bordercolor="#666666"
- borderopacity="1.0"
- inkscape:pageopacity="0"
- inkscape:pageshadow="2"
- inkscape:zoom="11.298641"
- inkscape:cx="8.3473457"
- inkscape:cy="69.793752"
- inkscape:document-units="px"
- inkscape:current-layer="layer25"
- showgrid="false"
- units="px"
- borderlayer="true"
- inkscape:showpageshadow="false"
- inkscape:snap-page="true"
- inkscape:snap-bbox="true"
- inkscape:bbox-nodes="true"
- inkscape:snap-bbox-edge-midpoints="true"
- inkscape:snap-bbox-midpoints="true"
- showguides="true"
- inkscape:guide-bbox="true"
- inkscape:window-width="1920"
- inkscape:window-height="1027"
- inkscape:window-x="-8"
- inkscape:window-y="-8"
- inkscape:window-maximized="1"
- inkscape:snap-center="true"
- inkscape:snap-object-midpoints="true"
- inkscape:measure-start="18.8975,12.498"
- inkscape:measure-end="20,12.498"
- inkscape:snap-intersection-paths="true"
- inkscape:object-paths="true"
- inkscape:snap-smooth-nodes="true"
- inkscape:snap-midpoints="true"
- inkscape:snap-global="true">
- <sodipodi:guide
- position="64,64"
- orientation="1,0"
- id="guide4039"
- inkscape:locked="false" />
- <sodipodi:guide
- position="64,64"
- orientation="0,1"
- id="guide3935"
- inkscape:locked="false"
- inkscape:label=""
- inkscape:color="rgb(0,0,255)" />
- </sodipodi:namedview>
- <metadata
- id="metadata5">
- <rdf:RDF>
- <cc:Work
- rdf:about="">
- <dc:format>image/svg+xml</dc:format>
- <dc:type
- rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
- <dc:title></dc:title>
- </cc:Work>
- </rdf:RDF>
- </metadata>
- <g
- inkscape:groupmode="layer"
- id="layer25"
- inkscape:label="Content"
- transform="translate(0,108)">
- <path
- sodipodi:type="star"
- style="display:inline;opacity:1;fill:#cd664d;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.99999994;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:normal"
- id="path3868"
- sodipodi:sides="3"
- sodipodi:cx="63.999996"
- sodipodi:cy="-43.999996"
- sodipodi:r1="64.000008"
- sodipodi:r2="32.000004"
- sodipodi:arg1="0"
- sodipodi:arg2="1.0471976"
- inkscape:flatsided="true"
- inkscape:rounded="0"
- inkscape:randomized="0"
- d="m 128,-43.999996 -96.000008,55.425632 0,-110.851265 z"
- inkscape:transform-center-x="-15.999997"
- inkscape:transform-center-y="3.8146973e-006" />
- </g>
-</svg>
diff --git a/src/stylesheets/Paper/Automata/shortcut.svg b/src/stylesheets/Paper/Automata/shortcut.svg
deleted file mode 100644
index 72b918ec..00000000
--- a/src/stylesheets/Paper/Automata/shortcut.svg
+++ /dev/null
@@ -1,94 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
- xmlns:dc="http://purl.org/dc/elements/1.1/"
- xmlns:cc="http://creativecommons.org/ns#"
- xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
- xmlns:svg="http://www.w3.org/2000/svg"
- xmlns="http://www.w3.org/2000/svg"
- xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
- xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
- width="20"
- height="20"
- viewBox="0 0 20 20"
- version="1.1"
- id="svg8"
- inkscape:export-filename="D:\Bethesda Game Modding\Mod Organizer 2\stylesheets\Paper\Dark\dots.png"
- inkscape:export-xdpi="307.20001"
- inkscape:export-ydpi="307.20001"
- inkscape:version="0.92.1 r15371"
- sodipodi:docname="shortcut.svg">
- <defs
- id="defs2" />
- <sodipodi:namedview
- id="base"
- pagecolor="#505050"
- bordercolor="#666666"
- borderopacity="1.0"
- inkscape:pageopacity="0"
- inkscape:pageshadow="2"
- inkscape:zoom="46.5"
- inkscape:cx="10"
- inkscape:cy="10"
- inkscape:document-units="px"
- inkscape:current-layer="layer25"
- showgrid="false"
- units="px"
- borderlayer="true"
- inkscape:showpageshadow="false"
- inkscape:snap-page="true"
- inkscape:snap-bbox="true"
- inkscape:bbox-nodes="true"
- inkscape:snap-bbox-edge-midpoints="true"
- inkscape:snap-bbox-midpoints="true"
- showguides="true"
- inkscape:guide-bbox="true"
- inkscape:window-width="1920"
- inkscape:window-height="1027"
- inkscape:window-x="-8"
- inkscape:window-y="-8"
- inkscape:window-maximized="1"
- inkscape:snap-center="true"
- inkscape:snap-object-midpoints="true"
- inkscape:measure-start="18.8975,12.498"
- inkscape:measure-end="20,12.498"
- inkscape:snap-intersection-paths="true"
- inkscape:object-paths="true"
- inkscape:snap-smooth-nodes="true"
- inkscape:snap-midpoints="true"
- inkscape:snap-global="true">
- <sodipodi:guide
- position="10,10"
- orientation="1,0"
- id="guide4039"
- inkscape:locked="false" />
- <sodipodi:guide
- position="10,10"
- orientation="0,1"
- id="guide3935"
- inkscape:locked="false" />
- </sodipodi:namedview>
- <metadata
- id="metadata5">
- <rdf:RDF>
- <cc:Work
- rdf:about="">
- <dc:format>image/svg+xml</dc:format>
- <dc:type
- rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
- <dc:title />
- </cc:Work>
- </rdf:RDF>
- </metadata>
- <g
- inkscape:groupmode="layer"
- id="layer25"
- inkscape:label="Content">
- <path
- inkscape:connector-curvature="0"
- style="display:inline;opacity:1;fill:#4d4a41;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:3;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:stroke fill markers"
- d="M 12,1.1035163 V 5.4492195 A 9.7951674,7.8360994 0 0 0 2.2050782,13.285156 9.7951674,7.8360994 0 0 0 6.9863282,20 H 8.201172 A 5.8526129,4.68207 0 0 1 6.1484376,16.4375 5.8526129,4.68207 0 0 1 12,11.757812 v 4.345704 l 8,-7.5 z"
- id="rect3929" />
- </g>
-</svg>
diff --git a/src/stylesheets/Paper/Automata/sort.svg b/src/stylesheets/Paper/Automata/sort.svg
deleted file mode 100644
index 32c08366..00000000
--- a/src/stylesheets/Paper/Automata/sort.svg
+++ /dev/null
@@ -1,125 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
- xmlns:dc="http://purl.org/dc/elements/1.1/"
- xmlns:cc="http://creativecommons.org/ns#"
- xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
- xmlns:svg="http://www.w3.org/2000/svg"
- xmlns="http://www.w3.org/2000/svg"
- xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
- xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
- width="20"
- height="20"
- viewBox="0 0 20 20"
- version="1.1"
- id="svg8"
- inkscape:export-filename="D:\Bethesda Game Modding\Mod Organizer 2\stylesheets\Paper\Dark\dots.png"
- inkscape:export-xdpi="307.20001"
- inkscape:export-ydpi="307.20001"
- inkscape:version="0.92.1 r15371"
- sodipodi:docname="sort.svg">
- <defs
- id="defs2" />
- <sodipodi:namedview
- id="base"
- pagecolor="#505050"
- bordercolor="#666666"
- borderopacity="1.0"
- inkscape:pageopacity="0"
- inkscape:pageshadow="2"
- inkscape:zoom="63.914761"
- inkscape:cx="0.055319431"
- inkscape:cy="1.8950951"
- inkscape:document-units="px"
- inkscape:current-layer="layer25"
- showgrid="false"
- units="px"
- borderlayer="true"
- inkscape:showpageshadow="false"
- inkscape:snap-page="true"
- inkscape:snap-bbox="true"
- inkscape:bbox-nodes="true"
- inkscape:snap-bbox-edge-midpoints="true"
- inkscape:snap-bbox-midpoints="true"
- showguides="true"
- inkscape:guide-bbox="true"
- inkscape:window-width="1920"
- inkscape:window-height="1027"
- inkscape:window-x="-8"
- inkscape:window-y="-8"
- inkscape:window-maximized="1"
- inkscape:snap-center="true"
- inkscape:snap-object-midpoints="true"
- inkscape:measure-start="18.8975,12.498"
- inkscape:measure-end="20,12.498"
- inkscape:snap-intersection-paths="true"
- inkscape:object-paths="true"
- inkscape:snap-smooth-nodes="true"
- inkscape:snap-midpoints="true"
- inkscape:snap-global="true">
- <sodipodi:guide
- position="10,10"
- orientation="1,0"
- id="guide4039"
- inkscape:locked="false" />
- <sodipodi:guide
- position="10,10"
- orientation="0,1"
- id="guide3935"
- inkscape:locked="false" />
- </sodipodi:namedview>
- <metadata
- id="metadata5">
- <rdf:RDF>
- <cc:Work
- rdf:about="">
- <dc:format>image/svg+xml</dc:format>
- <dc:type
- rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
- <dc:title />
- </cc:Work>
- </rdf:RDF>
- </metadata>
- <g
- inkscape:groupmode="layer"
- id="layer25"
- inkscape:label="Content">
- <g
- id="g3898"
- style="fill:#4d4a41;fill-opacity:1">
- <g
- id="g3886"
- style="fill:#4d4a41;fill-opacity:1">
- <rect
- y="4"
- x="13"
- height="16"
- width="3"
- id="rect4003"
- style="display:inline;opacity:1;fill:#4d4a41;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.40999997;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:normal" />
- <path
- style="display:inline;opacity:1;fill:#4d4a41;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.40999997;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:normal"
- d="m 13,0 v 7.9999998 h 7 z"
- id="path4009"
- inkscape:connector-curvature="0" />
- </g>
- <g
- id="g3890"
- style="fill:#4d4a41;fill-opacity:1">
- <rect
- style="display:inline;opacity:1;fill:#4d4a41;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.40999997;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:normal"
- id="rect3999"
- width="3"
- height="16"
- x="4"
- y="2.2775268e-008" />
- <path
- inkscape:connector-curvature="0"
- id="path4016"
- d="M 7,20 V 12 H 0 Z"
- style="display:inline;opacity:1;fill:#4d4a41;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.40999997;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:normal" />
- </g>
- </g>
- </g>
-</svg>
diff --git a/src/stylesheets/Paper/Dark/Arrows/down.svg b/src/stylesheets/Paper/Dark/Arrows/down.svg
deleted file mode 100644
index 21d34b87..00000000
--- a/src/stylesheets/Paper/Dark/Arrows/down.svg
+++ /dev/null
@@ -1,97 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
- xmlns:dc="http://purl.org/dc/elements/1.1/"
- xmlns:cc="http://creativecommons.org/ns#"
- xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
- xmlns:svg="http://www.w3.org/2000/svg"
- xmlns="http://www.w3.org/2000/svg"
- xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
- xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
- width="8"
- height="8"
- viewBox="0 0 8 8"
- version="1.1"
- id="svg8"
- inkscape:export-filename="D:\Bethesda Game Modding\Mod Organizer 2\stylesheets\Paper\Dark\dots.png"
- inkscape:export-xdpi="307.20001"
- inkscape:export-ydpi="307.20001"
- inkscape:version="0.92.1 r15371"
- sodipodi:docname="down.svg">
- <defs
- id="defs2" />
- <sodipodi:namedview
- id="base"
- pagecolor="#505050"
- bordercolor="#666666"
- borderopacity="1.0"
- inkscape:pageopacity="0"
- inkscape:pageshadow="2"
- inkscape:zoom="26.25481"
- inkscape:cx="0.53776101"
- inkscape:cy="5.6881881"
- inkscape:document-units="px"
- inkscape:current-layer="layer25"
- showgrid="false"
- units="px"
- borderlayer="true"
- inkscape:showpageshadow="false"
- inkscape:snap-page="true"
- inkscape:snap-bbox="true"
- inkscape:bbox-nodes="true"
- inkscape:snap-bbox-edge-midpoints="true"
- inkscape:snap-bbox-midpoints="true"
- showguides="true"
- inkscape:guide-bbox="true"
- inkscape:window-width="1920"
- inkscape:window-height="1027"
- inkscape:window-x="-8"
- inkscape:window-y="-8"
- inkscape:window-maximized="1"
- inkscape:snap-center="true"
- inkscape:snap-object-midpoints="true"
- inkscape:measure-start="18.8975,12.498"
- inkscape:measure-end="20,12.498"
- inkscape:snap-intersection-paths="true"
- inkscape:object-paths="true"
- inkscape:snap-smooth-nodes="true"
- inkscape:snap-midpoints="true"
- inkscape:snap-global="true">
- <sodipodi:guide
- position="4,4"
- orientation="1,0"
- id="guide4039"
- inkscape:locked="false" />
- <sodipodi:guide
- position="4,4"
- orientation="0,1"
- id="guide3935"
- inkscape:locked="false"
- inkscape:label=""
- inkscape:color="rgb(0,0,255)" />
- </sodipodi:namedview>
- <metadata
- id="metadata5">
- <rdf:RDF>
- <cc:Work
- rdf:about="">
- <dc:format>image/svg+xml</dc:format>
- <dc:type
- rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
- <dc:title></dc:title>
- </cc:Work>
- </rdf:RDF>
- </metadata>
- <g
- inkscape:groupmode="layer"
- id="layer25"
- inkscape:label="Content"
- transform="translate(0,-12)">
- <path
- style="display:inline;opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:212.1320343;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:1.93939996;stroke-opacity:1;paint-order:markers stroke fill"
- d="M 0,15 2,17 4,19 6,17 8,15 6,13 4,15 2,13 Z"
- id="rect3900-0"
- inkscape:connector-curvature="0" />
- </g>
-</svg>
diff --git a/src/stylesheets/Paper/Dark/Arrows/left.svg b/src/stylesheets/Paper/Dark/Arrows/left.svg
deleted file mode 100644
index b4aa8784..00000000
--- a/src/stylesheets/Paper/Dark/Arrows/left.svg
+++ /dev/null
@@ -1,97 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
- xmlns:dc="http://purl.org/dc/elements/1.1/"
- xmlns:cc="http://creativecommons.org/ns#"
- xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
- xmlns:svg="http://www.w3.org/2000/svg"
- xmlns="http://www.w3.org/2000/svg"
- xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
- xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
- width="8"
- height="8"
- viewBox="0 0 8 8"
- version="1.1"
- id="svg8"
- inkscape:export-filename="D:\Bethesda Game Modding\Mod Organizer 2\stylesheets\Paper\Dark\dots.png"
- inkscape:export-xdpi="307.20001"
- inkscape:export-ydpi="307.20001"
- inkscape:version="0.92.1 r15371"
- sodipodi:docname="left.svg">
- <defs
- id="defs2" />
- <sodipodi:namedview
- id="base"
- pagecolor="#505050"
- bordercolor="#666666"
- borderopacity="1.0"
- inkscape:pageopacity="0"
- inkscape:pageshadow="2"
- inkscape:zoom="26.25481"
- inkscape:cx="0.53776101"
- inkscape:cy="5.6881881"
- inkscape:document-units="px"
- inkscape:current-layer="layer25"
- showgrid="false"
- units="px"
- borderlayer="true"
- inkscape:showpageshadow="false"
- inkscape:snap-page="true"
- inkscape:snap-bbox="true"
- inkscape:bbox-nodes="true"
- inkscape:snap-bbox-edge-midpoints="true"
- inkscape:snap-bbox-midpoints="true"
- showguides="true"
- inkscape:guide-bbox="true"
- inkscape:window-width="1920"
- inkscape:window-height="1027"
- inkscape:window-x="-8"
- inkscape:window-y="-8"
- inkscape:window-maximized="1"
- inkscape:snap-center="true"
- inkscape:snap-object-midpoints="true"
- inkscape:measure-start="18.8975,12.498"
- inkscape:measure-end="20,12.498"
- inkscape:snap-intersection-paths="true"
- inkscape:object-paths="true"
- inkscape:snap-smooth-nodes="true"
- inkscape:snap-midpoints="true"
- inkscape:snap-global="true">
- <sodipodi:guide
- position="4,4"
- orientation="1,0"
- id="guide4039"
- inkscape:locked="false" />
- <sodipodi:guide
- position="4,4"
- orientation="0,1"
- id="guide3935"
- inkscape:locked="false"
- inkscape:label=""
- inkscape:color="rgb(0,0,255)" />
- </sodipodi:namedview>
- <metadata
- id="metadata5">
- <rdf:RDF>
- <cc:Work
- rdf:about="">
- <dc:format>image/svg+xml</dc:format>
- <dc:type
- rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
- <dc:title></dc:title>
- </cc:Work>
- </rdf:RDF>
- </metadata>
- <g
- inkscape:groupmode="layer"
- id="layer25"
- inkscape:label="Content"
- transform="translate(0,-12)">
- <path
- style="display:inline;opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:212.1320343;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:1.93939996;stroke-opacity:1;paint-order:markers stroke fill"
- d="M 5,20 3,18 1,16 3,14 5,12 7,14 5,16 7,18 Z"
- id="rect3900-0"
- inkscape:connector-curvature="0" />
- </g>
-</svg>
diff --git a/src/stylesheets/Paper/Dark/Arrows/right.svg b/src/stylesheets/Paper/Dark/Arrows/right.svg
deleted file mode 100644
index 37c9c20b..00000000
--- a/src/stylesheets/Paper/Dark/Arrows/right.svg
+++ /dev/null
@@ -1,97 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
- xmlns:dc="http://purl.org/dc/elements/1.1/"
- xmlns:cc="http://creativecommons.org/ns#"
- xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
- xmlns:svg="http://www.w3.org/2000/svg"
- xmlns="http://www.w3.org/2000/svg"
- xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
- xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
- width="8"
- height="8"
- viewBox="0 0 8 8"
- version="1.1"
- id="svg8"
- inkscape:export-filename="D:\Bethesda Game Modding\Mod Organizer 2\stylesheets\Paper\Dark\dots.png"
- inkscape:export-xdpi="307.20001"
- inkscape:export-ydpi="307.20001"
- inkscape:version="0.92.1 r15371"
- sodipodi:docname="right.svg">
- <defs
- id="defs2" />
- <sodipodi:namedview
- id="base"
- pagecolor="#505050"
- bordercolor="#666666"
- borderopacity="1.0"
- inkscape:pageopacity="0"
- inkscape:pageshadow="2"
- inkscape:zoom="26.25481"
- inkscape:cx="0.53776101"
- inkscape:cy="5.6881881"
- inkscape:document-units="px"
- inkscape:current-layer="layer25"
- showgrid="false"
- units="px"
- borderlayer="true"
- inkscape:showpageshadow="false"
- inkscape:snap-page="true"
- inkscape:snap-bbox="true"
- inkscape:bbox-nodes="true"
- inkscape:snap-bbox-edge-midpoints="true"
- inkscape:snap-bbox-midpoints="true"
- showguides="true"
- inkscape:guide-bbox="true"
- inkscape:window-width="1920"
- inkscape:window-height="1027"
- inkscape:window-x="-8"
- inkscape:window-y="-8"
- inkscape:window-maximized="1"
- inkscape:snap-center="true"
- inkscape:snap-object-midpoints="true"
- inkscape:measure-start="18.8975,12.498"
- inkscape:measure-end="20,12.498"
- inkscape:snap-intersection-paths="true"
- inkscape:object-paths="true"
- inkscape:snap-smooth-nodes="true"
- inkscape:snap-midpoints="true"
- inkscape:snap-global="true">
- <sodipodi:guide
- position="4,4"
- orientation="1,0"
- id="guide4039"
- inkscape:locked="false" />
- <sodipodi:guide
- position="4,4"
- orientation="0,1"
- id="guide3935"
- inkscape:locked="false"
- inkscape:label=""
- inkscape:color="rgb(0,0,255)" />
- </sodipodi:namedview>
- <metadata
- id="metadata5">
- <rdf:RDF>
- <cc:Work
- rdf:about="">
- <dc:format>image/svg+xml</dc:format>
- <dc:type
- rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
- <dc:title></dc:title>
- </cc:Work>
- </rdf:RDF>
- </metadata>
- <g
- inkscape:groupmode="layer"
- id="layer25"
- inkscape:label="Content"
- transform="translate(0,-12)">
- <path
- style="display:inline;opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:212.1320343;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:1.93939996;stroke-opacity:1;paint-order:markers stroke fill"
- d="M 3,20 5,18 7,16 5,14 3,12 1,14 3,16 1,18 Z"
- id="rect3900-0"
- inkscape:connector-curvature="0" />
- </g>
-</svg>
diff --git a/src/stylesheets/Paper/Dark/Arrows/up.svg b/src/stylesheets/Paper/Dark/Arrows/up.svg
deleted file mode 100644
index b5aed913..00000000
--- a/src/stylesheets/Paper/Dark/Arrows/up.svg
+++ /dev/null
@@ -1,97 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
- xmlns:dc="http://purl.org/dc/elements/1.1/"
- xmlns:cc="http://creativecommons.org/ns#"
- xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
- xmlns:svg="http://www.w3.org/2000/svg"
- xmlns="http://www.w3.org/2000/svg"
- xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
- xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
- width="8"
- height="8"
- viewBox="0 0 8 8"
- version="1.1"
- id="svg8"
- inkscape:export-filename="D:\Bethesda Game Modding\Mod Organizer 2\stylesheets\Paper\Dark\dots.png"
- inkscape:export-xdpi="307.20001"
- inkscape:export-ydpi="307.20001"
- inkscape:version="0.92.1 r15371"
- sodipodi:docname="up.svg">
- <defs
- id="defs2" />
- <sodipodi:namedview
- id="base"
- pagecolor="#505050"
- bordercolor="#666666"
- borderopacity="1.0"
- inkscape:pageopacity="0"
- inkscape:pageshadow="2"
- inkscape:zoom="26.25481"
- inkscape:cx="0.53776101"
- inkscape:cy="5.6881881"
- inkscape:document-units="px"
- inkscape:current-layer="layer25"
- showgrid="false"
- units="px"
- borderlayer="true"
- inkscape:showpageshadow="false"
- inkscape:snap-page="true"
- inkscape:snap-bbox="true"
- inkscape:bbox-nodes="true"
- inkscape:snap-bbox-edge-midpoints="true"
- inkscape:snap-bbox-midpoints="true"
- showguides="true"
- inkscape:guide-bbox="true"
- inkscape:window-width="1920"
- inkscape:window-height="1027"
- inkscape:window-x="-8"
- inkscape:window-y="-8"
- inkscape:window-maximized="1"
- inkscape:snap-center="true"
- inkscape:snap-object-midpoints="true"
- inkscape:measure-start="18.8975,12.498"
- inkscape:measure-end="20,12.498"
- inkscape:snap-intersection-paths="true"
- inkscape:object-paths="true"
- inkscape:snap-smooth-nodes="true"
- inkscape:snap-midpoints="true"
- inkscape:snap-global="true">
- <sodipodi:guide
- position="4,4"
- orientation="1,0"
- id="guide4039"
- inkscape:locked="false" />
- <sodipodi:guide
- position="4,4"
- orientation="0,1"
- id="guide3935"
- inkscape:locked="false"
- inkscape:label=""
- inkscape:color="rgb(0,0,255)" />
- </sodipodi:namedview>
- <metadata
- id="metadata5">
- <rdf:RDF>
- <cc:Work
- rdf:about="">
- <dc:format>image/svg+xml</dc:format>
- <dc:type
- rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
- <dc:title></dc:title>
- </cc:Work>
- </rdf:RDF>
- </metadata>
- <g
- inkscape:groupmode="layer"
- id="layer25"
- inkscape:label="Content"
- transform="translate(0,-12)">
- <path
- style="display:inline;opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:212.1320343;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:1.93939996;stroke-opacity:1;paint-order:markers stroke fill"
- d="m 0,17 2,-2 2,-2 2,2 2,2 -2,2 -2,-2 -2,2 z"
- id="rect3900-0"
- inkscape:connector-curvature="0" />
- </g>
-</svg>
diff --git a/src/stylesheets/Paper/Dark/Toolbar/archives.svg b/src/stylesheets/Paper/Dark/Toolbar/archives.svg
deleted file mode 100644
index 32758c87..00000000
--- a/src/stylesheets/Paper/Dark/Toolbar/archives.svg
+++ /dev/null
@@ -1,99 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
- xmlns:dc="http://purl.org/dc/elements/1.1/"
- xmlns:cc="http://creativecommons.org/ns#"
- xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
- xmlns:svg="http://www.w3.org/2000/svg"
- xmlns="http://www.w3.org/2000/svg"
- xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
- xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
- width="128"
- height="128"
- viewBox="0 0 128 128"
- version="1.1"
- id="svg8"
- inkscape:export-filename="D:\Bethesda Game Modding\Mod Organizer 2\stylesheets\Paper\Dark\dots.png"
- inkscape:export-xdpi="307.20001"
- inkscape:export-ydpi="307.20001"
- inkscape:version="0.92.1 r15371"
- sodipodi:docname="archives.svg">
- <defs
- id="defs2" />
- <sodipodi:namedview
- id="base"
- pagecolor="#505050"
- bordercolor="#666666"
- borderopacity="1.0"
- inkscape:pageopacity="0"
- inkscape:pageshadow="2"
- inkscape:zoom="4.3843014"
- inkscape:cx="83.662365"
- inkscape:cy="55.175634"
- inkscape:document-units="px"
- inkscape:current-layer="layer25"
- showgrid="false"
- units="px"
- borderlayer="true"
- inkscape:showpageshadow="false"
- inkscape:snap-page="true"
- inkscape:snap-bbox="true"
- inkscape:bbox-nodes="true"
- inkscape:snap-bbox-edge-midpoints="true"
- inkscape:snap-bbox-midpoints="true"
- showguides="true"
- inkscape:guide-bbox="true"
- inkscape:window-width="1920"
- inkscape:window-height="1027"
- inkscape:window-x="-8"
- inkscape:window-y="-8"
- inkscape:window-maximized="1"
- inkscape:snap-center="true"
- inkscape:snap-object-midpoints="true"
- inkscape:measure-start="18.8975,12.498"
- inkscape:measure-end="20,12.498"
- inkscape:snap-intersection-paths="true"
- inkscape:object-paths="true"
- inkscape:snap-smooth-nodes="true"
- inkscape:snap-midpoints="true"
- inkscape:snap-global="true"
- inkscape:lockguides="true">
- <sodipodi:guide
- position="64,64"
- orientation="1,0"
- id="guide4039"
- inkscape:locked="true" />
- <sodipodi:guide
- position="64,64"
- orientation="0,1"
- id="guide3935"
- inkscape:locked="true"
- inkscape:label=""
- inkscape:color="rgb(0,0,255)" />
- </sodipodi:namedview>
- <metadata
- id="metadata5">
- <rdf:RDF>
- <cc:Work
- rdf:about="">
- <dc:format>image/svg+xml</dc:format>
- <dc:type
- rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
- <dc:title />
- </cc:Work>
- </rdf:RDF>
- </metadata>
- <g
- inkscape:groupmode="layer"
- id="layer25"
- inkscape:label="Content"
- transform="translate(0,108)">
- <path
- style="opacity:1;fill:#d3d3d3;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:8;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:normal"
- d="m 73.998642,-83.999999 -9.998644,10.001356 H 14.001355 V -4.000001 H 113.99864 V -73.998643 L 104,-83.999999 H 88.999315 Z"
- id="path3849"
- inkscape:connector-curvature="0"
- sodipodi:nodetypes="ccccccccc" />
- </g>
-</svg>
diff --git a/src/stylesheets/Paper/Dark/Toolbar/executables.svg b/src/stylesheets/Paper/Dark/Toolbar/executables.svg
deleted file mode 100644
index c1b815c2..00000000
--- a/src/stylesheets/Paper/Dark/Toolbar/executables.svg
+++ /dev/null
@@ -1,99 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
- xmlns:dc="http://purl.org/dc/elements/1.1/"
- xmlns:cc="http://creativecommons.org/ns#"
- xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
- xmlns:svg="http://www.w3.org/2000/svg"
- xmlns="http://www.w3.org/2000/svg"
- xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
- xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
- width="128"
- height="128"
- viewBox="0 0 128 128"
- version="1.1"
- id="svg8"
- inkscape:export-filename="D:\Bethesda Game Modding\Mod Organizer 2\stylesheets\Paper\Dark\dots.png"
- inkscape:export-xdpi="307.20001"
- inkscape:export-ydpi="307.20001"
- inkscape:version="0.92.1 r15371"
- sodipodi:docname="executables.svg">
- <defs
- id="defs2" />
- <sodipodi:namedview
- id="base"
- pagecolor="#505050"
- bordercolor="#666666"
- borderopacity="1.0"
- inkscape:pageopacity="0"
- inkscape:pageshadow="2"
- inkscape:zoom="2.3629075"
- inkscape:cx="146.96542"
- inkscape:cy="0.84722465"
- inkscape:document-units="px"
- inkscape:current-layer="layer25"
- showgrid="false"
- units="px"
- borderlayer="true"
- inkscape:showpageshadow="false"
- inkscape:snap-page="true"
- inkscape:snap-bbox="true"
- inkscape:bbox-nodes="true"
- inkscape:snap-bbox-edge-midpoints="true"
- inkscape:snap-bbox-midpoints="true"
- showguides="true"
- inkscape:guide-bbox="true"
- inkscape:window-width="1920"
- inkscape:window-height="1027"
- inkscape:window-x="-8"
- inkscape:window-y="-8"
- inkscape:window-maximized="1"
- inkscape:snap-center="true"
- inkscape:snap-object-midpoints="true"
- inkscape:measure-start="18.8975,12.498"
- inkscape:measure-end="20,12.498"
- inkscape:snap-intersection-paths="true"
- inkscape:object-paths="true"
- inkscape:snap-smooth-nodes="true"
- inkscape:snap-midpoints="true"
- inkscape:snap-global="true">
- <sodipodi:guide
- position="64,64"
- orientation="1,0"
- id="guide4039"
- inkscape:locked="false" />
- <sodipodi:guide
- position="64,64"
- orientation="0,1"
- id="guide3935"
- inkscape:locked="false"
- inkscape:label=""
- inkscape:color="rgb(0,0,255)" />
- </sodipodi:namedview>
- <metadata
- id="metadata5">
- <rdf:RDF>
- <cc:Work
- rdf:about="">
- <dc:format>image/svg+xml</dc:format>
- <dc:type
- rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
- <dc:title />
- </cc:Work>
- </rdf:RDF>
- </metadata>
- <g
- inkscape:groupmode="layer"
- id="layer25"
- inkscape:label="Content"
- transform="translate(0,108)">
- <path
- inkscape:connector-curvature="0"
- style="display:inline;opacity:1;fill:#d3d3d3;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:3.00000048;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:stroke fill markers"
- d="m 69.224291,-99 v 25.297161 A 57.019516,45.615413 0 0 0 12.206203,-28.088371 57.019516,45.615413 0 0 0 40.038762,11 h 7.071835 A 34.069164,27.255211 0 0 1 35.161239,-9.7379829 34.069164,27.255211 0 0 1 69.224291,-36.979329 v 25.297161 L 115.7938,-55.341082 Z"
- id="rect3929"
- inkscape:transform-center-x="-6.418094"
- inkscape:transform-center-y="3.2118799" />
- </g>
-</svg>
diff --git a/src/stylesheets/Paper/Dark/Toolbar/help.svg b/src/stylesheets/Paper/Dark/Toolbar/help.svg
deleted file mode 100644
index 7f0024d5..00000000
--- a/src/stylesheets/Paper/Dark/Toolbar/help.svg
+++ /dev/null
@@ -1,128 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
- xmlns:dc="http://purl.org/dc/elements/1.1/"
- xmlns:cc="http://creativecommons.org/ns#"
- xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
- xmlns:svg="http://www.w3.org/2000/svg"
- xmlns="http://www.w3.org/2000/svg"
- xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
- xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
- width="128"
- height="128"
- viewBox="0 0 128 128"
- version="1.1"
- id="svg8"
- inkscape:export-filename="D:\Bethesda Game Modding\Mod Organizer 2\stylesheets\Paper\Dark\dots.png"
- inkscape:export-xdpi="307.20001"
- inkscape:export-ydpi="307.20001"
- inkscape:version="0.92.1 r15371"
- sodipodi:docname="help.svg">
- <defs
- id="defs2" />
- <sodipodi:namedview
- id="base"
- pagecolor="#505050"
- bordercolor="#666666"
- borderopacity="1.0"
- inkscape:pageopacity="0"
- inkscape:pageshadow="2"
- inkscape:zoom="3.6019536"
- inkscape:cx="85.162903"
- inkscape:cy="57.782228"
- inkscape:document-units="px"
- inkscape:current-layer="layer25"
- showgrid="false"
- units="px"
- borderlayer="true"
- inkscape:showpageshadow="false"
- inkscape:snap-page="true"
- inkscape:snap-bbox="true"
- inkscape:bbox-nodes="true"
- inkscape:snap-bbox-edge-midpoints="true"
- inkscape:snap-bbox-midpoints="true"
- showguides="true"
- inkscape:guide-bbox="true"
- inkscape:window-width="1920"
- inkscape:window-height="1027"
- inkscape:window-x="-8"
- inkscape:window-y="-8"
- inkscape:window-maximized="1"
- inkscape:snap-center="true"
- inkscape:snap-object-midpoints="true"
- inkscape:measure-start="18.8975,12.498"
- inkscape:measure-end="20,12.498"
- inkscape:snap-intersection-paths="true"
- inkscape:object-paths="true"
- inkscape:snap-smooth-nodes="true"
- inkscape:snap-midpoints="true"
- inkscape:snap-global="true">
- <sodipodi:guide
- position="64,64"
- orientation="1,0"
- id="guide4039"
- inkscape:locked="false" />
- <sodipodi:guide
- position="64,64"
- orientation="0,1"
- id="guide3935"
- inkscape:locked="false"
- inkscape:label=""
- inkscape:color="rgb(0,0,255)" />
- </sodipodi:namedview>
- <metadata
- id="metadata5">
- <rdf:RDF>
- <cc:Work
- rdf:about="">
- <dc:format>image/svg+xml</dc:format>
- <dc:type
- rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
- <dc:title />
- </cc:Work>
- </rdf:RDF>
- </metadata>
- <g
- inkscape:groupmode="layer"
- id="layer25"
- inkscape:label="Content"
- transform="translate(0,108)">
- <circle
- style="opacity:1;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:#d3d3d3;stroke-width:8;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:stroke fill markers"
- id="path3848"
- cx="64"
- cy="-44"
- r="60" />
- <g
- id="g3871"
- transform="translate(0,-1.242582)"
- style="stroke-width:1;fill:#d3d3d3;fill-opacity:1">
- <path
- id="path3851"
- d="m 64,-80 c -4.113298,1.67e-4 -7.447744,3.334612 -7.44791,7.44791 -6.7e-4,4.11389 3.33402,7.449258 7.44791,7.449426 4.11389,-1.68e-4 7.44858,-3.335536 7.44791,-7.449426 C 71.447743,-76.665387 68.113298,-79.999833 64,-80 Z"
- style="opacity:1;fill:#d3d3d3;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:8;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:stroke fill markers"
- inkscape:connector-curvature="0" />
- <path
- inkscape:connector-curvature="0"
- id="path3860"
- d="m 56.55209,-50.206086 v 34.757418 h 14.89582 v -34.757418 z"
- style="opacity:1;fill:#d3d3d3;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:8;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:stroke fill markers" />
- <path
- style="opacity:1;fill:#d3d3d3;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:8;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:stroke fill markers"
- d="m 64,-80 c -4.113298,1.67e-4 -7.447744,3.334612 -7.44791,7.44791 -6.7e-4,4.11389 3.33402,7.449258 7.44791,7.449426 4.11389,-1.68e-4 7.44858,-3.335536 7.44791,-7.449426 C 71.447743,-76.665387 68.113298,-79.999833 64,-80 Z"
- id="path3853"
- inkscape:connector-curvature="0" />
- <path
- inkscape:connector-curvature="0"
- id="path3862"
- d="m 64,-57.654754 c -4.113298,1.67e-4 -7.447744,3.334612 -7.44791,7.44791 -6.7e-4,4.11389 3.33402,7.449258 7.44791,7.449426 4.11389,-1.68e-4 7.44858,-3.335536 7.44791,-7.449426 -1.67e-4,-4.113297 -3.334612,-7.447743 -7.44791,-7.44791 z"
- style="opacity:1;fill:#d3d3d3;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:8;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:stroke fill markers" />
- <path
- style="opacity:1;fill:#d3d3d3;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:8;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:stroke fill markers"
- d="m 64,-22.897336 c -4.113298,1.67e-4 -7.447744,3.334612 -7.44791,7.44791 C 56.55142,-11.335536 59.88611,-8.000168 64,-8 c 4.11389,-1.68e-4 7.44858,-3.335536 7.44791,-7.449426 -1.67e-4,-4.113297 -3.334612,-7.447743 -7.44791,-7.44791 z"
- id="path3864"
- inkscape:connector-curvature="0" />
- </g>
- </g>
-</svg>
diff --git a/src/stylesheets/Paper/Dark/Toolbar/instances.svg b/src/stylesheets/Paper/Dark/Toolbar/instances.svg
deleted file mode 100644
index c5e58489..00000000
--- a/src/stylesheets/Paper/Dark/Toolbar/instances.svg
+++ /dev/null
@@ -1,98 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
- xmlns:dc="http://purl.org/dc/elements/1.1/"
- xmlns:cc="http://creativecommons.org/ns#"
- xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
- xmlns:svg="http://www.w3.org/2000/svg"
- xmlns="http://www.w3.org/2000/svg"
- xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
- xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
- width="128"
- height="128"
- viewBox="0 0 128 128"
- version="1.1"
- id="svg8"
- inkscape:export-filename="D:\Bethesda Game Modding\Mod Organizer 2\stylesheets\Paper\Dark\dots.png"
- inkscape:export-xdpi="307.20001"
- inkscape:export-ydpi="307.20001"
- inkscape:version="0.92.1 r15371"
- sodipodi:docname="instances.svg">
- <defs
- id="defs2" />
- <sodipodi:namedview
- id="base"
- pagecolor="#505050"
- bordercolor="#666666"
- borderopacity="1.0"
- inkscape:pageopacity="0"
- inkscape:pageshadow="2"
- inkscape:zoom="4.0674672"
- inkscape:cx="96.400649"
- inkscape:cy="-7.2667309"
- inkscape:document-units="px"
- inkscape:current-layer="layer25"
- showgrid="false"
- units="px"
- borderlayer="true"
- inkscape:showpageshadow="false"
- inkscape:snap-page="true"
- inkscape:snap-bbox="true"
- inkscape:bbox-nodes="true"
- inkscape:snap-bbox-edge-midpoints="true"
- inkscape:snap-bbox-midpoints="true"
- showguides="true"
- inkscape:guide-bbox="true"
- inkscape:window-width="1920"
- inkscape:window-height="1027"
- inkscape:window-x="-8"
- inkscape:window-y="-8"
- inkscape:window-maximized="1"
- inkscape:snap-center="true"
- inkscape:snap-object-midpoints="true"
- inkscape:measure-start="18.8975,12.498"
- inkscape:measure-end="20,12.498"
- inkscape:snap-intersection-paths="true"
- inkscape:object-paths="true"
- inkscape:snap-smooth-nodes="true"
- inkscape:snap-midpoints="true"
- inkscape:snap-global="true">
- <sodipodi:guide
- position="64,64"
- orientation="1,0"
- id="guide4039"
- inkscape:locked="false" />
- <sodipodi:guide
- position="64,64"
- orientation="0,1"
- id="guide3935"
- inkscape:locked="false"
- inkscape:label=""
- inkscape:color="rgb(0,0,255)" />
- </sodipodi:namedview>
- <metadata
- id="metadata5">
- <rdf:RDF>
- <cc:Work
- rdf:about="">
- <dc:format>image/svg+xml</dc:format>
- <dc:type
- rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
- <dc:title />
- </cc:Work>
- </rdf:RDF>
- </metadata>
- <g
- inkscape:groupmode="layer"
- id="layer25"
- inkscape:label="Content"
- transform="translate(0,108)">
- <path
- style="opacity:1;fill:#d3d3d3;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:8;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:normal"
- d="m 43.074486,-84 c -7.191305,0.179697 -13.681107,2.360737 -19.604836,6.184844 l -0.829325,3.561678 -4.167425,3.208112 -5.581697,14.982451 c -5.7982022,15.544107 -18.3828084,44.900541 4.037437,52.0629145 5.467067,-4.389774 11.744564,-10.4769935 18.039776,-16.6021055 3.729503,-3.62877 7.012953,-4.081632 14.160925,-4.081632 h 14.865463 14.865462 c 7.147976,0 10.431426,0.452872 14.160926,4.081632 6.2952,6.125112 12.583088,12.2123315 18.050168,16.6021055 22.42024,-7.1623735 9.83564,-36.5188075 4.03744,-52.0629145 l -5.5921,-14.982451 -4.15702,-3.208112 -0.82933,-3.561678 C 98.606622,-81.639263 92.106422,-83.820123 84.915122,-84 l -5.46211,2.979332 H 63.994804 48.546994 Z m 51.652146,11.766541 c 2.28943,-7.75e-4 4.14597,1.854604 4.14663,4.144028 7.8e-4,2.290441 -1.85618,4.147401 -4.14663,4.146627 -2.28942,-6.62e-4 -4.1448,-1.857203 -4.14402,-4.146627 6.6e-4,-2.288409 1.85562,-4.143366 4.14402,-4.144028 z m -61.453263,4.144028 c 4.578848,-1.12e-4 8.290765,3.711805 8.290653,8.290653 1.13e-4,4.578848 -3.711805,8.290766 -8.290653,8.290655 -4.578848,1.13e-4 -8.290768,-3.711807 -8.290654,-8.290655 -1.12e-4,-4.578848 3.711806,-8.290766 8.290654,-8.290653 z m 53.162613,4.146627 c 2.28943,-7.75e-4 4.14597,1.854603 4.14663,4.144026 7.7e-4,2.290441 -1.85619,4.147403 -4.14663,4.146627 -2.289423,-6.62e-4 -4.144799,-1.857203 -4.144025,-4.146627 6.62e-4,-2.288409 1.855618,-4.143365 4.144025,-4.144026 z m 16.581308,0 c 2.28942,-7.75e-4 4.14596,1.854603 4.14663,4.144026 7.7e-4,2.290441 -1.85619,4.147403 -4.14663,4.146627 -2.28943,-6.62e-4 -4.144798,-1.857203 -4.144028,-4.146627 6.6e-4,-2.288409 1.855618,-4.143365 4.144028,-4.144026 z m -8.290658,8.290654 c 2.28943,-7.75e-4 4.14597,1.854602 4.14663,4.144027 7.8e-4,2.29044 -1.85618,4.147401 -4.14663,4.146626 -2.28942,-6.62e-4 -4.1448,-1.857203 -4.14402,-4.146626 6.6e-4,-2.28841 1.85562,-4.143366 4.14402,-4.144027 z m -48.662414,6.184843 h 5.527101 v 5.527103 h 5.527103 v 5.527102 H 51.591319 V -32.886 h -5.527101 v -5.527102 h -5.527103 v -5.527102 h 5.527103 z m 33.10802,0 c 4.578848,-1.12e-4 8.290764,3.711806 8.290654,8.290654 1.1e-4,4.578848 -3.711806,8.290765 -8.290654,8.290653 -4.578848,1.13e-4 -8.290766,-3.711805 -8.290654,-8.290653 -1.13e-4,-4.578848 3.711806,-8.290767 8.290654,-8.290654 z"
- id="path3849"
- inkscape:connector-curvature="0"
- sodipodi:nodetypes="cccccccscscccccccccccccccccccccccccccccccccccccccccccccccccccccc" />
- </g>
-</svg>
diff --git a/src/stylesheets/Paper/Dark/Toolbar/nexus.svg b/src/stylesheets/Paper/Dark/Toolbar/nexus.svg
deleted file mode 100644
index 9bf09e14..00000000
--- a/src/stylesheets/Paper/Dark/Toolbar/nexus.svg
+++ /dev/null
@@ -1,122 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
- xmlns:dc="http://purl.org/dc/elements/1.1/"
- xmlns:cc="http://creativecommons.org/ns#"
- xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
- xmlns:svg="http://www.w3.org/2000/svg"
- xmlns="http://www.w3.org/2000/svg"
- xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
- xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
- width="128"
- height="128"
- viewBox="0 0 128 128"
- version="1.1"
- id="svg8"
- inkscape:export-filename="D:\Bethesda Game Modding\Mod Organizer 2\stylesheets\Paper\Dark\dots.png"
- inkscape:export-xdpi="307.20001"
- inkscape:export-ydpi="307.20001"
- inkscape:version="0.92.1 r15371"
- sodipodi:docname="nexus.svg">
- <defs
- id="defs2" />
- <sodipodi:namedview
- id="base"
- pagecolor="#505050"
- bordercolor="#666666"
- borderopacity="1.0"
- inkscape:pageopacity="0"
- inkscape:pageshadow="2"
- inkscape:zoom="3.5008328"
- inkscape:cx="88.067867"
- inkscape:cy="37.493768"
- inkscape:document-units="px"
- inkscape:current-layer="layer25"
- showgrid="false"
- units="px"
- borderlayer="true"
- inkscape:showpageshadow="false"
- inkscape:snap-page="true"
- inkscape:snap-bbox="true"
- inkscape:bbox-nodes="true"
- inkscape:snap-bbox-edge-midpoints="true"
- inkscape:snap-bbox-midpoints="true"
- showguides="true"
- inkscape:guide-bbox="true"
- inkscape:window-width="1920"
- inkscape:window-height="1027"
- inkscape:window-x="-8"
- inkscape:window-y="-8"
- inkscape:window-maximized="1"
- inkscape:snap-center="true"
- inkscape:snap-object-midpoints="true"
- inkscape:measure-start="108.073,108.073"
- inkscape:measure-end="112.737,112.737"
- inkscape:snap-intersection-paths="true"
- inkscape:object-paths="true"
- inkscape:snap-smooth-nodes="true"
- inkscape:snap-midpoints="true"
- inkscape:snap-global="true">
- <sodipodi:guide
- position="64,64"
- orientation="1,0"
- id="guide4039"
- inkscape:locked="false" />
- <sodipodi:guide
- position="64,64"
- orientation="0,1"
- id="guide3935"
- inkscape:locked="false"
- inkscape:label="255"
- inkscape:color="rgb(0,0,255)" />
- </sodipodi:namedview>
- <metadata
- id="metadata5">
- <rdf:RDF>
- <cc:Work
- rdf:about="">
- <dc:format>image/svg+xml</dc:format>
- <dc:type
- rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
- <dc:title />
- </cc:Work>
- </rdf:RDF>
- </metadata>
- <g
- inkscape:groupmode="layer"
- id="layer25"
- inkscape:label="Content"
- transform="translate(0,108)">
- <path
- style="opacity:1;fill:#d3d3d3;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:7.99999952;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:normal"
- d="m 37.578906,-103.99986 -2.327345,1.09219 c -5.625077,2.65309 -12.341703,6.959572 -18.124217,14.742184 l -1.439062,1.942967 -0.3,2.397657 c -0.727944,5.785703 0.645537,10.927203 2.601562,15.550782 -3.97001,7.523426 -6.539675,15.713366 -6.546095,24.274216 h 0.01176 v 0.06094 c 0.01254,2.097981 0.399663,4.072256 0.646875,6.100783 -3.5620623,4.427533 -7.0313403,9.956846 -7.8375002,17.704685 l -0.2648844,2.554689 1.0921859,2.327343 c 2.6531016,5.6250757 6.9595777,12.341692 14.7421887,18.12422 l 1.94297,1.4390639 2.397656,0.3 c 5.785701,0.727944 10.927203,-0.6455399 15.55078,-2.6015644 7.523428,3.9700084 15.713368,6.539664 24.274217,6.546096 v -0.012 h 0.06094 c 2.097982,-0.0126 4.072257,-0.39966 6.100783,-0.646872 4.427536,3.5620555 9.956847,7.0313275 17.704687,7.8374995 l 2.554687,0.26484 2.327343,-1.092192 C 98.373512,12.254575 105.09014,7.9480995 110.87265,0.1654875 l 1.43672,-1.9429715 0.30236,-2.3976556 c 0.72794,-5.7854267 -0.64555,-10.9269284 -2.60157,-15.5505044 3.97001,-7.523428 6.53967,-15.713368 6.5461,-24.27422 h -0.012 v -0.06094 c -0.0133,-2.097984 -0.39967,-4.072258 -0.64688,-6.100783 3.56206,-4.427535 7.03131,-9.956848 7.8375,-17.704688 L 124,-70.420959 l -1.09219,-2.327345 c -2.6531,-5.625076 -6.95957,-12.341702 -14.74219,-18.124218 l -1.94297,-1.436719 -2.39765,-0.302345 c -5.785706,-0.727943 -10.927203,0.64554 -15.550778,2.601564 -7.523434,-3.970005 -15.713374,-6.539667 -24.274225,-6.546088 v 0.01172 h -0.06094 c -2.097982,0.01266 -4.072255,0.399662 -6.100782,0.646874 -4.427534,-3.562058 -9.956847,-7.031344 -17.704687,-7.837494 z m 1.485937,6.351563 c 0.03984,-7.78e-4 0.07969,-7.78e-4 0.119532,0 0.08844,8.72e-4 0.176806,0.0056 0.264842,0.01406 0.01563,-1.23e-4 0.03128,-1.23e-4 0.04686,0 0.06038,0.0056 0.120558,0.01378 0.180468,0.02344 0.03369,0.0037 0.06016,0.01978 0.09376,0.02344 8.119439,0.924618 13.438311,4.913385 17.695312,8.847652 0.518092,0.475264 0.857314,1.113992 0.960938,1.809374 1.846847,-0.259602 3.708531,-0.399736 5.573432,-0.419528 7.414774,0.0056 14.704137,1.913317 21.171093,5.540624 3.592503,-2.496611 7.615515,-4.335787 12.384376,-4.79531 1.744997,-0.168149 3.586634,-0.159114 5.503124,0.082 0.5255,0.06779 1.0245,0.270451 1.44845,0.588283 6.86031,5.097232 10.33844,10.548846 12.76641,15.658591 0.0245,0.05156 0.0622,0.100836 0.0867,0.152346 0.0157,0.04031 0.0306,0.08093 0.0445,0.121879 0.0412,0.09035 0.0779,0.182649 0.11015,0.276564 0,0.0029 0,0.0063 0,0.0094 4e-5,0.0086 4e-5,0.01718 0,0.02577 0.0284,0.09024 0.0527,0.18174 0.0727,0.274217 0.0197,0.09143 0.0355,0.18371 0.0469,0.276565 1e-5,0.0079 1e-5,0.01562 0,0.02344 0.005,0.06 0.0108,0.120187 0.0141,0.180468 7.7e-4,0.03984 7.7e-4,0.07969 0,0.119532 -8.7e-4,0.08844 -0.005,0.176808 -0.0141,0.264844 1.2e-4,0.01563 1.2e-4,0.03125 0,0.04685 -0.008,0.06038 -0.0139,0.12056 -0.0233,0.180468 -0.004,0.03369 -0.0199,0.06016 -0.0234,0.09376 -0.92463,8.119438 -4.91339,13.438312 -8.84766,17.695313 -0.47526,0.51809 -1.114,0.857312 -1.80939,0.960935 0.25962,1.846853 0.39976,3.708535 0.41955,5.573439 -0.005,7.414773 -1.91333,14.704135 -5.54064,21.171093 2.49662,3.592503 4.33578,7.615515 4.79533,12.384375 0.16814,1.7449932 0.1591,3.586632 -0.0819,5.5031256 -0.0677,0.5254956 -0.27045,1.0244952 -0.58828,1.4484372 -5.09716,6.8603052 -10.548768,10.3384447 -15.658515,12.7664047 -0.05156,0.02448 -0.100836,0.06228 -0.152342,0.08676 -0.04031,0.01572 -0.08092,0.0306 -0.12188,0.04452 -0.09035,0.04116 -0.182648,0.07788 -0.276562,0.11016 -0.0031,0 -0.0063,0 -0.0094,0 -0.0086,3.5e-5 -0.01718,3.5e-5 -0.02577,0 -0.09024,0.02844 -0.181741,0.05268 -0.274216,0.07272 -0.09143,0.0198 -0.183711,0.03552 -0.276563,0.04692 -0.0079,2.4e-5 -0.01563,2.4e-5 -0.02344,0 -0.06,0.006 -0.120186,0.0108 -0.180468,0.01404 -0.03984,7.8e-4 -0.0797,7.8e-4 -0.119532,0 -0.08844,-8.64e-4 -0.176806,-0.006 -0.264846,-0.01404 -0.01563,1.2e-4 -0.03128,1.2e-4 -0.04686,0 -0.06037,-0.006 -0.120559,-0.0138 -0.180469,-0.0234 -0.03369,-0.0036 -0.06014,-0.0198 -0.09376,-0.0234 C 80.110654,8.6632245 74.79178,4.6744608 70.534779,0.740188 c -0.51809,-0.47526 -0.857313,-1.113984 -0.960936,-1.8093745 -1.847084,0.259404 -3.708765,0.3995425 -5.573668,0.4193305 -7.414776,-0.006 -14.704135,-1.9133172 -21.171094,-5.5406236 -3.592503,2.4966096 -7.615515,4.3357863 -12.384377,4.7953119 -1.744993,0.1681465 -3.586632,0.1591105 -5.503122,-0.082008 -0.525498,-0.067788 -1.024497,-0.2704523 -1.448438,-0.5882844 -6.860299,-5.0972295 -10.338443,-10.5488439 -12.766408,-15.6585929 -0.0245,-0.05156 -0.06234,-0.10086 -0.08672,-0.152343 -0.0157,-0.0403 -0.03056,-0.08095 -0.04453,-0.12188 -0.04117,-0.09035 -0.07795,-0.182646 -0.11016,-0.276563 -6e-6,-0.0031 -6e-6,-0.0063 0,-0.0094 -3.6e-5,-0.0086 -3.6e-5,-0.01718 0,-0.02577 -0.02843,-0.09024 -0.05268,-0.181742 -0.07265,-0.274218 -0.01984,-0.09143 -0.03547,-0.183709 -0.04685,-0.276562 -3.1e-5,-0.0077 -3.1e-5,-0.01562 0,-0.02344 -0.0067,-0.06 -0.01116,-0.120186 -0.0141,-0.180469 -7.76e-4,-0.03984 -7.76e-4,-0.07969 0,-0.119532 8.7e-4,-0.08844 0.0053,-0.176808 0.0141,-0.264847 -1.19e-4,-0.01563 -1.19e-4,-0.03125 0,-0.04685 0.006,-0.06037 0.0138,-0.120559 0.02344,-0.180469 0.0037,-0.03369 0.01981,-0.06014 0.02344,-0.09376 0.924627,-8.119438 4.91339,-13.438313 8.847658,-17.695312 0.475263,-0.518093 1.113993,-0.857315 1.809373,-0.960938 -0.259589,-1.846868 -0.399724,-3.708551 -0.419516,-5.573451 0.0056,-7.414775 1.913317,-14.704137 5.540624,-21.171094 -2.49661,-3.592502 -4.335787,-7.615515 -4.79531,-12.384377 -0.168149,-1.744994 -0.159113,-3.58663 0.082,-5.503124 0.06779,-0.525498 0.270452,-1.024496 0.588283,-1.448437 5.097232,-6.860299 10.548845,-10.338443 15.658591,-12.766403 0.05156,-0.02447 0.100837,-0.06234 0.152347,-0.08672 0.04031,-0.01575 0.08093,-0.03056 0.121878,-0.04453 0.09035,-0.04116 0.182646,-0.07791 0.276563,-0.110157 0.0029,0 0.0063,0 0.0094,0 0.0086,-3.7e-5 0.01718,-3.7e-5 0.02578,0 0.09024,-0.02841 0.181741,-0.05269 0.274218,-0.07266 0.09143,-0.01987 0.183711,-0.03544 0.276563,-0.04687 0.0077,-2.8e-5 0.01563,-2.8e-5 0.02344,0 0.06,-0.0066 0.120186,-0.01116 0.180468,-0.01416 z m 0.119532,3.070311 c -4.918893,2.320027 -9.830756,5.400922 -14.6625,11.903905 -0.106116,0.843338 -0.161184,1.665537 -0.171096,2.467968 -0.0098,0.802433 0.02693,1.584662 0.100776,2.350782 1.033521,10.725648 10.216452,17.923996 18.292968,25.258593 l -3.764063,4.246876 21.255469,-0.131254 -2.578125,-20.934377 -4.446092,5.025 c -7.369823,-8.23868 -18.074776,-20.906037 -14.02734,-30.187495 z m 3.009373,0.623437 c -0.04253,0.205977 -0.106176,0.407033 -0.189841,0.599999 -0.355764,0.815838 -0.552587,1.702983 -0.602346,2.660157 -0.01586,0.305199 -0.01586,0.61855 0,0.9375 0.111036,2.232626 0.98277,4.789133 2.3625,7.502343 3.563011,-1.890229 7.372419,-3.27389 11.317968,-4.110937 l 0.3,-0.119533 c -3.54753,-3.278554 -7.503737,-6.172443 -13.188279,-7.46953 z m 12.2625,4.582031 -0.215624,0.07031 -0.01176,0.02344 c 0.08105,-0.01726 0.146146,-0.07651 0.227344,-0.09376 z m 45.750002,5.723437 c -0.802434,-0.0098 -1.584666,0.02696 -2.350781,0.100775 -10.725652,1.03352 -17.923998,10.216453 -25.258595,18.292968 l -4.246878,-3.764057 0.131254,21.255469 20.934376,-2.578123 -5.025,-4.446096 c 8.238683,-7.369823 20.906044,-18.072433 30.187494,-14.025 -2.32001,-4.918892 -5.40091,-9.833098 -11.9039,-14.664842 -0.84333,-0.106117 -1.66554,-0.161182 -2.46797,-0.171094 z m 9.55079,17.050783 c -2.23265,0.111035 -4.78915,0.982766 -7.50235,2.362499 1.89023,3.563012 3.27389,7.372418 4.11093,11.317966 l 0.11965,0.3 c 3.27856,-3.547531 6.17244,-7.503737 7.46954,-13.18828 -0.20598,-0.04252 -0.40703,-0.106175 -0.6,-0.189842 -0.81584,-0.355764 -1.70298,-0.552585 -2.66016,-0.602343 -0.30519,-0.01586 -0.61855,-0.01586 -0.9375,0 z m -0.47813,12.827343 c 0.0173,0.08105 0.0764,0.146144 0.0937,0.227342 l -0.0702,-0.215625 z m -49.762503,13.535156 -20.932032,2.578124 5.025,4.446095 c -8.238684,7.369824 -20.906042,18.074776 -30.1875,14.027343 2.320026,4.918894 5.400921,9.8307519 11.903905,14.662499 0.843337,0.106116 1.665539,0.1611829 2.467968,0.1710949 0.802431,0.00984 1.584663,-0.026952 2.350782,-0.100776 10.72565,-1.0335217 17.923997,-10.2164539 25.258593,-18.2929699 l 4.246874,3.764056 z m 29.503121,0.585935 -21.255467,0.133592 2.578124,20.932032 4.446095,-5.025 c 7.369824,8.238685 18.072432,20.9060398 14.024999,30.1875015 4.91889,-2.3200323 9.833102,-5.4009245 14.664841,-11.9039095 0.10608,-0.8433371 0.16119,-1.6655363 0.1711,-2.4679691 0.01,-0.8024304 -0.027,-1.5846612 -0.1008,-2.3507799 C 102.5149,-20.870044 93.331956,-28.06839 85.255441,-35.402988 Z m -67.504684,4.267971 c -3.278554,3.547531 -6.172444,7.503737 -7.469533,13.18828 0.205978,0.04256 0.407034,0.106176 0.6,0.189843 0.815838,0.355762 1.702981,0.552586 2.660158,0.602343 0.305198,0.01586 0.618549,0.01586 0.9375,0 2.232626,-0.111036 4.789133,-0.982768 7.502342,-2.3625 -1.890227,-3.56301 -3.273887,-7.372418 -4.110936,-11.317966 z m -2.8875,0.92578 0.0703,0.215625 0.02344,0.01176 c -0.01726,-0.08105 -0.0765,-0.146143 -0.09375,-0.227344 z m 65.608592,28.7109367 c -3.563012,1.8902291 -7.37242,3.2738893 -11.317967,4.1109373 l -0.3,0.119532 c 3.54753,3.2785476 7.503737,6.1724429 13.188278,7.4695349 0.04252,-0.20598 0.106177,-0.407028 0.189844,-0.6 0.355763,-0.815844 0.552585,-1.7029799 0.602345,-2.6601593 0.01586,-0.305196 0.01586,-0.618552 0,-0.9375 -0.111036,-2.2326384 -0.982768,-4.789134 -2.3625,-7.5023449 z M 73.771091,1.2790394 c -0.08105,0.01728 -0.146145,0.07644 -0.227344,0.09372 l 0.215627,-0.07032 z"
- id="path3849"
- inkscape:connector-curvature="0"
- sodipodi:nodetypes="cccccccccccccccccccccccccccccccccccccccccccccccccccccccscccccccccccccccccccccscccccccccccccccccccccscccccccccccccccccccccscccccccccccccccccccccccccsccccccccccccccccccccccccccccccccccccccccccccccccccccccsccccccccccccccccccc" />
- <g
- style="fill:#505050;fill-opacity:1;stroke-width:0.19550784"
- id="g4412"
- inkscape:transform-center-x="-5.0913502"
- inkscape:transform-center-y="-4.4596393"
- transform="matrix(5.1148849,0,0,5.1148849,12.851148,-95.148848)" />
- <g
- transform="matrix(0,4.9102895,-4.9102895,0,113.1029,-93.102895)"
- inkscape:transform-center-y="24"
- inkscape:transform-center-x="-21.022466"
- id="g4064"
- style="fill:#505050;fill-opacity:1;stroke-width:0.20365398" />
- <g
- style="fill:#505050;fill-opacity:1;stroke-width:0.20365398"
- id="g4068"
- inkscape:transform-center-x="24.000005"
- inkscape:transform-center-y="21.022466"
- transform="matrix(-4.9102895,0,0,-4.9102895,113.10289,5.1029002)" />
- <g
- transform="matrix(-2.719147e-8,-4.9102894,4.9102894,-2.719147e-8,14.8971,5.1028901)"
- inkscape:transform-center-y="-24.000003"
- inkscape:transform-center-x="21.022466"
- id="g4072"
- style="fill:#505050;fill-opacity:1;stroke-width:0.20365398" />
- </g>
-</svg>
diff --git a/src/stylesheets/Paper/Dark/Toolbar/problems.svg b/src/stylesheets/Paper/Dark/Toolbar/problems.svg
deleted file mode 100644
index 6cb095aa..00000000
--- a/src/stylesheets/Paper/Dark/Toolbar/problems.svg
+++ /dev/null
@@ -1,132 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
- xmlns:dc="http://purl.org/dc/elements/1.1/"
- xmlns:cc="http://creativecommons.org/ns#"
- xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
- xmlns:svg="http://www.w3.org/2000/svg"
- xmlns="http://www.w3.org/2000/svg"
- xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
- xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
- width="128"
- height="128"
- viewBox="0 0 128 128"
- version="1.1"
- id="svg8"
- inkscape:export-filename="D:\Bethesda Game Modding\Mod Organizer 2\stylesheets\Paper\Dark\dots.png"
- inkscape:export-xdpi="307.20001"
- inkscape:export-ydpi="307.20001"
- inkscape:version="0.92.1 r15371"
- sodipodi:docname="problems.svg">
- <defs
- id="defs2" />
- <sodipodi:namedview
- id="base"
- pagecolor="#505050"
- bordercolor="#666666"
- borderopacity="1.0"
- inkscape:pageopacity="0"
- inkscape:pageshadow="2"
- inkscape:zoom="2.7490221"
- inkscape:cx="29.143293"
- inkscape:cy="24.756816"
- inkscape:document-units="px"
- inkscape:current-layer="layer25"
- showgrid="false"
- units="px"
- borderlayer="true"
- inkscape:showpageshadow="false"
- inkscape:snap-page="true"
- inkscape:snap-bbox="true"
- inkscape:bbox-nodes="true"
- inkscape:snap-bbox-edge-midpoints="true"
- inkscape:snap-bbox-midpoints="true"
- showguides="false"
- inkscape:guide-bbox="true"
- inkscape:window-width="1920"
- inkscape:window-height="1027"
- inkscape:window-x="-8"
- inkscape:window-y="-8"
- inkscape:window-maximized="1"
- inkscape:snap-center="true"
- inkscape:snap-object-midpoints="true"
- inkscape:measure-start="18.8975,12.498"
- inkscape:measure-end="20,12.498"
- inkscape:snap-intersection-paths="true"
- inkscape:object-paths="true"
- inkscape:snap-smooth-nodes="true"
- inkscape:snap-midpoints="true"
- inkscape:snap-global="true">
- <sodipodi:guide
- position="64,64"
- orientation="1,0"
- id="guide4039"
- inkscape:locked="false" />
- <sodipodi:guide
- position="64,64"
- orientation="0,1"
- id="guide3935"
- inkscape:locked="false"
- inkscape:label=""
- inkscape:color="rgb(0,0,255)" />
- </sodipodi:namedview>
- <metadata
- id="metadata5">
- <rdf:RDF>
- <cc:Work
- rdf:about="">
- <dc:format>image/svg+xml</dc:format>
- <dc:type
- rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
- <dc:title />
- </cc:Work>
- </rdf:RDF>
- </metadata>
- <g
- inkscape:groupmode="layer"
- id="layer25"
- inkscape:label="Content"
- transform="translate(0,108)">
- <g
- id="g3861"
- transform="matrix(0.55290621,0,0,0.55290621,28.614002,-29.750801)"
- style="fill:#737373;fill-opacity:1;stroke-width:1.80862486" />
- <path
- style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:#d3d3d3;stroke-width:7.99999952;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
- d="m 64.344931,-96.263216 c -2.808173,-0.131523 -5.455193,1.310482 -6.861125,3.737706 L 4.9422487,-1.7926865 C 2.0494853,3.2027814 5.6651359,9.4471972 11.450616,9.4476002 H 116.53373 c 5.78548,-4.1e-4 9.40112,-6.2448225 6.50836,-11.2402867 L 70.500541,-92.52551 c -1.279824,-2.209515 -3.599319,-3.61792 -6.15561,-3.737706 z"
- id="path3875"
- inkscape:connector-curvature="0"
- sodipodi:nodetypes="cccccccc" />
- <g
- transform="matrix(-0.83333333,0,0,-0.83333333,117.3255,-70.7681)"
- id="g3871"
- style="stroke-width:1.20000005;fill:#d3d3d3;fill-opacity:1">
- <path
- inkscape:connector-curvature="0"
- id="path3851"
- d="m 64,-80 c -4.113298,1.67e-4 -7.447744,3.334612 -7.44791,7.44791 -6.7e-4,4.11389 3.33402,7.449258 7.44791,7.449426 4.11389,-1.68e-4 7.44858,-3.335536 7.44791,-7.449426 C 71.447743,-76.665387 68.113298,-79.999833 64,-80 Z"
- style="opacity:1;fill:#d3d3d3;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:9.60000038;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:stroke fill markers" />
- <path
- inkscape:connector-curvature="0"
- id="path3860-8"
- d="m 56.55209,-50.206086 v 34.757418 h 14.89582 v -34.757418 z"
- style="opacity:1;fill:#d3d3d3;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:9.60000038;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:stroke fill markers" />
- <path
- inkscape:connector-curvature="0"
- style="opacity:1;fill:#d3d3d3;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:9.60000038;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:stroke fill markers"
- d="m 64,-80 c -4.113298,1.67e-4 -7.447744,3.334612 -7.44791,7.44791 -6.7e-4,4.11389 3.33402,7.449258 7.44791,7.449426 4.11389,-1.68e-4 7.44858,-3.335536 7.44791,-7.449426 C 71.447743,-76.665387 68.113298,-79.999833 64,-80 Z"
- id="path3853" />
- <path
- inkscape:connector-curvature="0"
- id="path3862"
- d="m 64,-57.654754 c -4.113298,1.67e-4 -7.447744,3.334612 -7.44791,7.44791 -6.7e-4,4.11389 3.33402,7.449258 7.44791,7.449426 4.11389,-1.68e-4 7.44858,-3.335536 7.44791,-7.449426 -1.67e-4,-4.113297 -3.334612,-7.447743 -7.44791,-7.44791 z"
- style="opacity:1;fill:#d3d3d3;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:9.60000038;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:stroke fill markers" />
- <path
- style="opacity:1;fill:#d3d3d3;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:9.60000038;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:stroke fill markers"
- d="m 64,-22.897336 c -4.113298,1.67e-4 -7.447744,3.334612 -7.44791,7.44791 C 56.55142,-11.335536 59.88611,-8.000168 64,-8 c 4.11389,-1.68e-4 7.44858,-3.335536 7.44791,-7.449426 -1.67e-4,-4.113297 -3.334612,-7.447743 -7.44791,-7.44791 z"
- id="path3864"
- inkscape:connector-curvature="0" />
- </g>
- </g>
-</svg>
diff --git a/src/stylesheets/Paper/Dark/Toolbar/profiles.svg b/src/stylesheets/Paper/Dark/Toolbar/profiles.svg
deleted file mode 100644
index 3679449a..00000000
--- a/src/stylesheets/Paper/Dark/Toolbar/profiles.svg
+++ /dev/null
@@ -1,112 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
- xmlns:dc="http://purl.org/dc/elements/1.1/"
- xmlns:cc="http://creativecommons.org/ns#"
- xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
- xmlns:svg="http://www.w3.org/2000/svg"
- xmlns="http://www.w3.org/2000/svg"
- xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
- xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
- width="128"
- height="128"
- viewBox="0 0 128 128"
- version="1.1"
- id="svg8"
- inkscape:export-filename="D:\Bethesda Game Modding\Mod Organizer 2\stylesheets\Paper\Dark\dots.png"
- inkscape:export-xdpi="307.20001"
- inkscape:export-ydpi="307.20001"
- inkscape:version="0.92.1 r15371"
- sodipodi:docname="profiles.svg">
- <defs
- id="defs2" />
- <sodipodi:namedview
- id="base"
- pagecolor="#505050"
- bordercolor="#666666"
- borderopacity="1.0"
- inkscape:pageopacity="0"
- inkscape:pageshadow="2"
- inkscape:zoom="3.7735292"
- inkscape:cx="65.045731"
- inkscape:cy="44.931547"
- inkscape:document-units="px"
- inkscape:current-layer="layer25"
- showgrid="false"
- units="px"
- borderlayer="true"
- inkscape:showpageshadow="false"
- inkscape:snap-page="true"
- inkscape:snap-bbox="true"
- inkscape:bbox-nodes="true"
- inkscape:snap-bbox-edge-midpoints="true"
- inkscape:snap-bbox-midpoints="true"
- showguides="true"
- inkscape:guide-bbox="true"
- inkscape:window-width="1920"
- inkscape:window-height="1027"
- inkscape:window-x="-8"
- inkscape:window-y="-8"
- inkscape:window-maximized="1"
- inkscape:snap-center="true"
- inkscape:snap-object-midpoints="true"
- inkscape:measure-start="64,11.9981"
- inkscape:measure-end="64,7.93514"
- inkscape:snap-intersection-paths="true"
- inkscape:object-paths="true"
- inkscape:snap-smooth-nodes="true"
- inkscape:snap-midpoints="true"
- inkscape:snap-global="true"
- inkscape:lockguides="true"
- inkscape:snap-nodes="false">
- <sodipodi:guide
- position="64,64"
- orientation="1,0"
- id="guide4039"
- inkscape:locked="true" />
- <sodipodi:guide
- position="64,64"
- orientation="0,1"
- id="guide3935"
- inkscape:locked="true"
- inkscape:label=""
- inkscape:color="rgb(0,0,255)" />
- </sodipodi:namedview>
- <metadata
- id="metadata5">
- <rdf:RDF>
- <cc:Work
- rdf:about="">
- <dc:format>image/svg+xml</dc:format>
- <dc:type
- rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
- <dc:title />
- </cc:Work>
- </rdf:RDF>
- </metadata>
- <g
- inkscape:groupmode="layer"
- id="layer25"
- inkscape:label="Content"
- transform="translate(0,108)">
- <g
- id="g3856"
- style="fill:#d3d3d3;fill-opacity:1;stroke-width:0.81111115"
- transform="matrix(1.2328767,0,0,1.2328767,-14.376763,-8.863014)">
- <circle
- r="21"
- cy="-44"
- cx="64"
- id="path3874"
- style="opacity:1;fill:#d3d3d3;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:3.24444389;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:stroke fill markers" />
- <path
- sodipodi:nodetypes="ccsscc"
- inkscape:connector-curvature="0"
- id="path3858"
- transform="translate(0,-108)"
- d="M 63.421875,89.005859 C 48.016021,89.029098 33.691706,97.71624 25.455078,112.03125 29.673503,114.55644 34.640748,116 40,116 h 48 c 5.001681,0 9.660535,-1.26009 13.68945,-3.48047 C 93.502774,97.918077 79.022242,89.020453 63.421875,89.005859 Z"
- style="opacity:1;fill:#d3d3d3;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.40555558;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:normal" />
- </g>
- </g>
-</svg>
diff --git a/src/stylesheets/Paper/Dark/Toolbar/settings.svg b/src/stylesheets/Paper/Dark/Toolbar/settings.svg
deleted file mode 100644
index c58bd3dd..00000000
--- a/src/stylesheets/Paper/Dark/Toolbar/settings.svg
+++ /dev/null
@@ -1,95 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
- xmlns:dc="http://purl.org/dc/elements/1.1/"
- xmlns:cc="http://creativecommons.org/ns#"
- xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
- xmlns:svg="http://www.w3.org/2000/svg"
- xmlns="http://www.w3.org/2000/svg"
- xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
- xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
- width="128"
- height="128"
- viewBox="0 0 128 128"
- version="1.1"
- id="svg8"
- inkscape:export-filename="D:\Bethesda Game Modding\Mod Organizer 2\stylesheets\Paper\Dark\dots.png"
- inkscape:export-xdpi="307.20001"
- inkscape:export-ydpi="307.20001"
- inkscape:version="0.92.3 (2405546, 2018-03-11)"
- sodipodi:docname="settings.svg">
- <defs
- id="defs2" />
- <sodipodi:namedview
- id="base"
- pagecolor="#505050"
- bordercolor="#666666"
- borderopacity="1.0"
- inkscape:pageopacity="0"
- inkscape:pageshadow="2"
- inkscape:zoom="1.9728069"
- inkscape:cx="-7.9211047"
- inkscape:cy="7.7686433"
- inkscape:document-units="px"
- inkscape:current-layer="layer25"
- showgrid="false"
- units="px"
- borderlayer="true"
- inkscape:showpageshadow="false"
- inkscape:snap-page="true"
- inkscape:snap-bbox="true"
- inkscape:bbox-nodes="true"
- inkscape:snap-bbox-edge-midpoints="true"
- inkscape:snap-bbox-midpoints="true"
- showguides="true"
- inkscape:guide-bbox="true"
- inkscape:window-width="1920"
- inkscape:window-height="1027"
- inkscape:window-x="-8"
- inkscape:window-y="-8"
- inkscape:window-maximized="1"
- inkscape:snap-center="true"
- inkscape:snap-object-midpoints="true"
- inkscape:measure-start="8,64"
- inkscape:measure-end="17.9742,64"
- inkscape:snap-intersection-paths="true"
- inkscape:object-paths="true"
- inkscape:snap-smooth-nodes="true"
- inkscape:snap-midpoints="true"
- inkscape:snap-global="true">
- <sodipodi:guide
- position="64,64"
- orientation="1,0"
- id="guide4039"
- inkscape:locked="false" />
- <sodipodi:guide
- position="64,64"
- orientation="0,1"
- id="guide3854"
- inkscape:locked="false" />
- </sodipodi:namedview>
- <metadata
- id="metadata5">
- <rdf:RDF>
- <cc:Work
- rdf:about="">
- <dc:format>image/svg+xml</dc:format>
- <dc:type
- rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
- <dc:title />
- </cc:Work>
- </rdf:RDF>
- </metadata>
- <g
- inkscape:groupmode="layer"
- id="layer25"
- inkscape:label="Content"
- transform="translate(0,108)">
- <path
- style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#d3d3d3;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:6;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
- d="m 55.75,-99 v 4.125 7.80957 c -5.874077,1.1212 -11.426558,3.41583 -16.379154,6.76757 l -8.427243,-8.42724 -11.668701,11.6687 8.429938,8.42993 c -3.363624,4.949109 -5.670511,10.500523 -6.802499,16.37647 H 9 v 16.5 h 11.934571 c 1.121197,5.874077 3.41583,11.426558 6.767574,16.379154 l -8.427243,8.427243 11.668701,11.668701 8.429938,-8.429938 C 44.32265,-4.341216 49.874053,-2.034329 55.75,-0.902341 V 11 h 16.5 V -0.934571 c 5.87408,-1.121197 11.42656,-3.41583 16.37915,-6.767574 l 8.42724,8.427243 11.66871,-11.668701 -8.42994,-8.429938 c 3.36362,-4.949109 5.67052,-10.500512 6.80249,-16.376459 H 119 v -16.5 h -11.93458 c -1.12119,-5.874077 -3.41581,-11.426558 -6.76757,-16.37915 l 8.42725,-8.42725 -11.66871,-11.6687 -8.42994,8.42993 c -4.9491,-3.36361 -10.5005,-5.6705 -16.37645,-6.80249 V -99 Z M 64,-68.75 c 13.6039,0 24.75,11.146102 24.75,24.75 0,13.603898 -11.1461,24.75 -24.75,24.75 -13.603898,0 -24.75,-11.146102 -24.75,-24.75 0,-13.603898 11.146102,-24.75 24.75,-24.75 z m 0,11 c -0.47872,0 -0.94996,0.0231 -1.415282,0.0693 -6.488691,0.65604 -11.608839,5.776199 -12.26489,12.26489 -1.87e-4,0.0018 1.76e-4,0.0036 0,0.0052 -0.0462,0.46365 -0.0693,0.93302 -0.0693,1.409903 0,0.47663 0.0231,0.94611 0.0693,1.409914 1.76e-4,0.0018 -1.76e-4,0.0036 0,0.0052 0.65604,6.488694 5.776199,11.608842 12.26489,12.264893 0.0018,1.76e-4 0.0036,-1.76e-4 0.0052,0 0.46365,0.0462 0.93302,0.0693 1.409903,0.0693 0.47685,0 0.94633,-0.0242 1.409899,-0.0693 0.002,-1.76e-4 0.004,1.76e-4 0.005,0 6.4887,-0.65604 11.60884,-5.776199 12.26491,-12.264893 0.0473,-0.46552 0.0693,-0.93676 0.0693,-1.415293 0,-0.4785 -0.0231,-0.94974 -0.0693,-1.415282 -0.65604,-6.488746 -5.77621,-11.608894 -12.26491,-12.264945 -0.002,-1.76e-4 -0.004,1.76e-4 -0.005,0 -0.463745,-0.0462 -0.933225,-0.0704 -1.409877,-0.0704 z"
- id="path3915"
- inkscape:connector-curvature="0" />
- </g>
-</svg>
diff --git a/src/stylesheets/Paper/Dark/Toolbar/tools.svg b/src/stylesheets/Paper/Dark/Toolbar/tools.svg
deleted file mode 100644
index ac492405..00000000
--- a/src/stylesheets/Paper/Dark/Toolbar/tools.svg
+++ /dev/null
@@ -1,117 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
- xmlns:dc="http://purl.org/dc/elements/1.1/"
- xmlns:cc="http://creativecommons.org/ns#"
- xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
- xmlns:svg="http://www.w3.org/2000/svg"
- xmlns="http://www.w3.org/2000/svg"
- xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
- xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
- width="128"
- height="128"
- viewBox="0 0 128 128"
- version="1.1"
- id="svg8"
- inkscape:export-filename="D:\Bethesda Game Modding\Mod Organizer 2\stylesheets\Paper\Dark\dots.png"
- inkscape:export-xdpi="307.20001"
- inkscape:export-ydpi="307.20001"
- inkscape:version="0.92.1 r15371"
- sodipodi:docname="tools.svg">
- <defs
- id="defs2" />
- <sodipodi:namedview
- id="base"
- pagecolor="#505050"
- bordercolor="#666666"
- borderopacity="1.0"
- inkscape:pageopacity="0"
- inkscape:pageshadow="2"
- inkscape:zoom="2.668288"
- inkscape:cx="78.127687"
- inkscape:cy="1.523437"
- inkscape:document-units="px"
- inkscape:current-layer="layer25"
- showgrid="false"
- units="px"
- borderlayer="true"
- inkscape:showpageshadow="false"
- inkscape:snap-page="true"
- inkscape:snap-bbox="true"
- inkscape:bbox-nodes="true"
- inkscape:snap-bbox-edge-midpoints="true"
- inkscape:snap-bbox-midpoints="true"
- showguides="true"
- inkscape:guide-bbox="true"
- inkscape:window-width="1920"
- inkscape:window-height="1027"
- inkscape:window-x="-8"
- inkscape:window-y="-8"
- inkscape:window-maximized="1"
- inkscape:snap-center="true"
- inkscape:snap-object-midpoints="true"
- inkscape:measure-start="54,64"
- inkscape:measure-end="64,64"
- inkscape:snap-intersection-paths="true"
- inkscape:object-paths="true"
- inkscape:snap-smooth-nodes="true"
- inkscape:snap-midpoints="true"
- inkscape:snap-global="true"
- inkscape:bbox-paths="true">
- <sodipodi:guide
- position="64,64"
- orientation="1,0"
- id="guide4039"
- inkscape:locked="false" />
- <sodipodi:guide
- position="64,64"
- orientation="0,1"
- id="guide3935"
- inkscape:locked="false"
- inkscape:label=""
- inkscape:color="rgb(0,0,255)" />
- </sodipodi:namedview>
- <metadata
- id="metadata5">
- <rdf:RDF>
- <cc:Work
- rdf:about="">
- <dc:format>image/svg+xml</dc:format>
- <dc:type
- rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
- <dc:title />
- </cc:Work>
- </rdf:RDF>
- </metadata>
- <g
- inkscape:groupmode="layer"
- id="layer25"
- inkscape:label="Content"
- transform="translate(0,108)">
- <g
- id="g3997"
- transform="matrix(0.55904332,-0.55901902,-0.55904332,-0.55901902,4.4028105,-32.039863)"
- style="fill:#d3d3d3;fill-opacity:1;stroke-width:1.26487899">
- <path
- id="path3965"
- transform="translate(0,-108)"
- d="M -8,52 A 12,12 0 0 0 -20,64 12,12 0 0 0 -8,76 H 44 V 52 Z"
- style="opacity:1;fill:#d3d3d3;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:5.05951595;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:stroke fill markers"
- inkscape:connector-curvature="0" />
- <path
- id="rect3969"
- transform="translate(0,-108)"
- d="m 128,53 -18,5 H 84 v 12 h 26 l 18,5 18,-5 V 58 Z"
- style="opacity:1;fill:#d3d3d3;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:5.05951595;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:stroke fill markers"
- inkscape:connector-curvature="0" />
- <path
- style="opacity:1;fill:#d3d3d3;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2.86491537;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:stroke fill markers"
- d="m 32.291016,50.410156 c -5.753358,0.0068 -10.878972,3.636181 -12.796875,9.060547 h 12.796875 c 2.50182,-5.4e-5 4.529351,2.027478 4.529296,4.529297 -5.4e-5,2.501819 -2.027476,4.531196 -4.529296,4.53125 H 19.496094 c 1.917396,5.423518 7.042374,9.051114 12.794922,9.058594 5.30786,-0.0064 10.126181,-3.102522 12.33789,-7.927735 H 64 105.23759 v -0.0039 c 0.0293,0.0015 0.0586,0.0028 0.0879,0.0039 3.12696,-3.3e-4 5.66178,-2.535151 5.6621,-5.662109 -3.2e-4,-3.126962 -2.53514,-5.661785 -5.66211,-5.662109 -0.0299,0.0011 -0.0599,0.0024 -0.0898,0.0039 v -0.0039 H 64 44.626953 c -2.211168,-4.82439 -7.028882,-7.920713 -12.335937,-7.927735 z"
- transform="matrix(-3.8373246e-5,-1.7660262,-1.7660262,-3.8373246e-5,177.02814,69.028134)"
- id="rect3945"
- inkscape:connector-curvature="0"
- sodipodi:nodetypes="cccsccccccccccccccc" />
- </g>
- </g>
-</svg>
diff --git a/src/stylesheets/Paper/Dark/Toolbar/update.svg b/src/stylesheets/Paper/Dark/Toolbar/update.svg
deleted file mode 100644
index a6c272be..00000000
--- a/src/stylesheets/Paper/Dark/Toolbar/update.svg
+++ /dev/null
@@ -1,141 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
- xmlns:dc="http://purl.org/dc/elements/1.1/"
- xmlns:cc="http://creativecommons.org/ns#"
- xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
- xmlns:svg="http://www.w3.org/2000/svg"
- xmlns="http://www.w3.org/2000/svg"
- xmlns:xlink="http://www.w3.org/1999/xlink"
- xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
- xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
- width="128"
- height="128"
- viewBox="0 0 128 128"
- version="1.1"
- id="svg8"
- inkscape:export-filename="D:\Bethesda Game Modding\Mod Organizer 2\stylesheets\Paper\Dark\dots.png"
- inkscape:export-xdpi="307.20001"
- inkscape:export-ydpi="307.20001"
- inkscape:version="0.92.3 (2405546, 2018-03-11)"
- sodipodi:docname="update.svg">
- <defs
- id="defs2">
- <clipPath
- clipPathUnits="userSpaceOnUse"
- id="clipPath4152-4-4-9">
- <rect
- style="fill:#fdf6e3;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:3;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
- id="rect4154-6-3-0"
- width="598.57141"
- height="928.57141"
- x="352.85715"
- y="22.362183" />
- </clipPath>
- </defs>
- <sodipodi:namedview
- id="base"
- pagecolor="#505050"
- bordercolor="#666666"
- borderopacity="1.0"
- inkscape:pageopacity="0"
- inkscape:pageshadow="2"
- inkscape:zoom="7.2039069"
- inkscape:cx="73.953922"
- inkscape:cy="61.867245"
- inkscape:document-units="px"
- inkscape:current-layer="layer25"
- showgrid="false"
- units="px"
- borderlayer="true"
- inkscape:showpageshadow="false"
- inkscape:snap-page="true"
- inkscape:snap-bbox="true"
- inkscape:bbox-nodes="true"
- inkscape:snap-bbox-edge-midpoints="true"
- inkscape:snap-bbox-midpoints="true"
- showguides="true"
- inkscape:guide-bbox="true"
- inkscape:window-width="1920"
- inkscape:window-height="1027"
- inkscape:window-x="-8"
- inkscape:window-y="-8"
- inkscape:window-maximized="1"
- inkscape:snap-center="true"
- inkscape:snap-object-midpoints="true"
- inkscape:measure-start="18.8975,12.498"
- inkscape:measure-end="20,12.498"
- inkscape:snap-intersection-paths="true"
- inkscape:object-paths="true"
- inkscape:snap-smooth-nodes="true"
- inkscape:snap-midpoints="true"
- inkscape:snap-global="true">
- <sodipodi:guide
- position="64,64"
- orientation="1,0"
- id="guide4039"
- inkscape:locked="false" />
- <sodipodi:guide
- position="64,64"
- orientation="0,1"
- id="guide3935"
- inkscape:locked="false"
- inkscape:label=""
- inkscape:color="rgb(0,0,255)" />
- </sodipodi:namedview>
- <metadata
- id="metadata5">
- <rdf:RDF>
- <cc:Work
- rdf:about="">
- <dc:format>image/svg+xml</dc:format>
- <dc:type
- rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
- <dc:title />
- </cc:Work>
- </rdf:RDF>
- </metadata>
- <g
- inkscape:groupmode="layer"
- id="layer25"
- inkscape:label="Content"
- transform="translate(0,108)">
- <g
- id="g4217-3-45"
- transform="matrix(0.50121193,0,0,0.50121193,-6.8260092,-112.83314)"
- style="display:inline;fill:#006868;fill-opacity:1"
- inkscape:export-xdpi="96"
- inkscape:export-ydpi="96">
- <g
- style="display:none;fill:#006868;fill-opacity:1"
- transform="translate(0,-783.36216)"
- id="layer1-9-66"
- inkscape:label="Reference">
- <image
- width="269"
- height="269"
- preserveAspectRatio="none"
- xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQ0AAAENCAYAAAAVEjAIAAAABHNCSVQICAgIfAhkiAAAIABJREFU eJzsnXd4HNXVh987s129WZJ7t+VuA8amGNOLA6GXQEIgQAotBAghJAQSEgKEJAQSekvoYHpJ4ANT jcEN27hXucjqfbVlduZ+f8zMatVsrSRbtjzv84iyO3PnzuzMb84599xzxaJSKXFwcHDoJEpvd8DB wWH/whENBweHpHBEw8HBISkc0XBwcEgKRzQcHBySwhENBweHpHBEw8HBISkc0XBwcEgKRzQcHByS whENBweHpHBEw8HBISkc0XBwcEgKRzQcHBySwhENBweHpHBEw8HBISkc0XBwcEgKRzQcHBySwhEN BweHpHBEw8HBISkc0XBwcEgKRzQcHBySwhENBweHpHBEw8HBISkc0XBwcEgKRzQcHBySwhENBweH pHBEw8HBISkc0XBwcEgKRzQcHBySwhENBweHpHBEw8HBISkc0XBwcEgKRzQcHBySwhENBweHpHBE w8HBISkc0XBwcEgKRzQcHBySwhENBweHpHBEw8HBISkc0XBwcEgKRzQcHBySwhENBweHpHBEw8HB ISkc0XBwcEgKRzQcHBySwhENBweHpHBEw8HBISkc0XBwcEgKRzQcHBySwhENBweHpHBEw8HBISkc 0XBwcEgKRzQcHBySwrW3DqRrEUqKN7J9yyaKN6xhS/FmYtEoUsq91YV9EsXlwuf1kZaaQkZGJjn9 CkjLLSQ3L4/C/gMJZOT0dhf3eaSh01hdxs6SEioryglWl1JdXUVDfT2NwSZC4TCGrmPoMRSXC5fb g9vtJjMtlfSMDLKycwhkF5Kfn09BYSGBzH69fUr7NHtMNKLBWjZv2sjObVvYsGo5WzZvpL6mimBj A5FQiFhMwzAMOMBFAwAhEEKgqioutwev14cvECCQkkpmVi6jxoxh4PAxDBk+kvyBQ1Fcnt7uca8S bqhha/Fmdm5Zz+YNa9m8aSPB+jpCTUFCoSa0SIRYTEPX9V3eX0JRUBQFt9uD2+vF5w8QCKSQmpnF 6NFjGDxiDP2HjmTg4GG4fSl78Qz3bcSi0p59andsXse7r77A1o1rqamupKmxgVBTkHCoiUg4jBaN oMdiGNIwRcMBACEEiqIghILL5cLlduNyufF4fXj9fnz+AGnpGaRnZjNi9BhGjp1E4eChDBo8BHcg o7e7v0dpqC5n86aNlBavZ+3K5ezYVkxDXS3BxgbCoSaikTBRWyhisU7fX8ISa0UoqAnX3O314vOZ 1zyQlk56eiZDh49g9PjJ5A8axrARo/Gl9u1rvit6TDRkLMr8j9/nvbnPUbJtC3U11YSaGlv+mLqO YehIKU23xLEymhHC+lfzjSwUBVVVUV0u3G4PLrcHnyUgPn+AQGoaaemZDBwylKLxExk4ooiho4rw +AK9fDLdo6a8hM3rV7N94xpWr1pJTVUF9bU1BBNeQNFIGC0aRddj8XtLGgYSmdz9ZV3vxGuuKJaI uNx4vF48Xh8+fwB/IIVAahrpGVnk5RcybsIE+g8by4gx40jJyN7zF2YfoUdEo76mijeeeZgFn82j rGQ79XU1hENNxDQNw9DjbogdvzjQ4xi7Q1gCYv+3UBTLElHjIuJyuXF7PLg93vgNnZqeQU5ePmOK JjDxoBmMHDuOQGZeL55J5zBiGts2rWXdiiV8u3wZpTuKqa2uosmyJCKRcFwk7BeQoevN1kTCvQVd u79aX3Msy08RCkqCcLs9HtP68/rwBVJITUsnIyuHQcNGMH7SVMZMnEr+oOEI0XfHGLotGuuWfcUr /3mC9auXU1m2k4b6WqLhMDE9hrTMQ0ckuo9IsETau6FdLjdenw+vz48/JdV6GxYw5aBDmTxjFiPH TgDV3ctn0ZKash2sWPg58z+dR9nO7dRWV8Vdjkg41EIkpGGgW1bq3noBdeaauz0e85pbVkh2Th4D Bg/liKOPp2jKoaRm5e6x/vUW3RKN9cu/5t8P3cem9aupKi8l2FCPpkXNH9kRij2ObVYn3tC2b+7x +gikpJKalkFWbh6jxo5n9gnfYeyU6bh70X2RukbxxrV88eF7rFjyNRVlO2mwXI9IOISmRU0L1bIk Wr94evO+ai0iQghURUVRVfOae7x4fX4CqWlkZuWQV9Cfgw8/ihlHHkv+oGEIRe21vvckXRaN+poq /vmnX/PtNwup2LmDYGMDmhY1/UpHMHoFW0SEoqAqlknt8bZ0XcZNZPaJpzLh4MNQPb691jepa2xY uZSP//smq1Z8Q1V5GfV1NTQFG+NWRWtXdn+4j1oLt6q64tZHSlp6PP4x5eBDmXXCqQwcMXa/F48u iYahRXj2oXv56L9vsXO7Gcl2BGPfIh7cU1VU1YXHa4tHJrn9Chg3aSonnnY2oyYfusf7Urx2Oe+/ +TIrli4yXdi6WpqCjUQjYWKahm7o+5VQdESigLjUBIvPCp72KxzA9MOO5Og5Z5E/aHhvd7fLdClP Y/7H77Pgs3lUlu2MuySOYOxbyISH0NB1YjGNSDhMU7CRxvo6aqoq2LBuNdMPn82cs75HRl7/Hu9D sKaC9996mfnzPqBkezF1VnAzEgmj6zEMw+hT902i6GmGQUyPEY1GCIeaCDbUU19bTWV5KcuWLeHY E05l1gnfwZOS3su9Tp6kRWPH5nX899XnKSvZTmNDnSMY+zjxG9kwMHQdXY+hRaPxG7mmsoLVK5Zy 8hnnc9jRJ/VIsNSIRVn21We888qzbN6wlsryUhrr6+IxC/t+6cv3jJQSaQ0D26IdjYQJNQVprK+l unQnyxZ/yWnn/ZCR46fsVy5L0qLxztznKdm2hfq6GiKhEIau9+kfvy8hpWweidBjaNZbsLG+jsqy UlYu/Zqzvn8F2QWDunyM+spS3nzxKb76/GPKdmyjtqaKcFOQaDQSv1cOpPuljcWnaUQs8airrWbH 1mJmn/gdTvjuufhSM3u7u50iKdGINNawbdM66mqqzTwMy8R02L8wrDe9YRjosWYTurG+jq1bNnHO RZcxaebRSbe7ctEXvPbcE2xYs5Kq8lIaG+qIhMPEYtoBb402i4eGIc3rHgmb4lFfV8Pmdas5/cLL GDJ6fG93dbckJRpbNm2ktqaKUFOQmKY5GZ37Mbb5rFn5DzHNNJ/DoSYef+AeDl+1nFPP+yH+TqRL h5sa+b83XmTef99ie/FGaqoqCAUb0bToAWld7Arb4tAsa892W5qCjZSWlTLn9HOYccwcXB5vb3e1 Q5ISjdLtxWbgMxqJp4M77N/YN3HUCDcHTCNhGupq2bZ5E2df+EOGFE3tcP+dm1Yz99knWbZ4ARWl JdTXWtnAjnXRIYkuS9hoQo+ZcaZIKERdVQWb1q1mzjnfJ6dwcG93tV2SEo11q5YTagqawSznZugz 2DdwLKZhNFkui+V3l5Vs48yLfsT0I49DcTe//aSusWT+x7z+/JNsWr+GmspyGhvqiUbCjnXRSewA tWbFe8wRrhDBxgZKtm/lzIsuY/SEabCPBUk7LRq6FqF480Yi4VA8mObcFH0LO9YRNgxi9tsvHOLp B//Gum+XcvYPrySQnkU42MBbzz/Gpx/+j53biqmtrjRd1pjmBMaTpEWso6lljKmqopyTTz+HI086 E7d37yXi7Y5Oi0ZJ8UYaaqsJh0JmnQKHPokd64hJSVOj6XdHIxGC9XWU7yxhzrkX88EbL7B04QKq yktpqK8166PozoukO9huopnDYsaYtGiEuc82Ul1VxZyzL8KfntXb3QSSEI1tWzbS2FBPzMnLOCAw rY6W7kptTRVLvvqCYLDRnKKeMJTqjKJ1n0TBDhkGhqGjRaO89/qL7Ny2hXMv+Rn5g0f0djc7LxrF G9aYs1c1c8jIoe/T4u0XixFqClJbXWnmecRijnWxh0gU7HhCXiRCJBLmwh//nMIho3q1f50Wja3F xabPKnsmm++oPz1D2pAxAIjdbNuXkYAWCVNdup1YTSXh0q1Ey7dSU7aDph2biVTtjM/07JX+JQzN xmJavFjQvjRXRHF78PYbiC+vP9n9BuDrNxBXbiHurH54MnPIzm1Z81MaBlXlZWh1VcTqKtHKtxIq L6GqdDvRqp1Eyrf36jWHloJt1Brous7ShaZFd9FPrqP/0NG91rdOi0ZMi5rVkXroJkntP4zMoWN6 pK2+QN6YyS3+PxJqQmuoIVxTSemyBVQv/5zK9d8SKtmCNPZ+TGlfEQgAlz+FlOFFFIyaRPrEwygo moIrJR1XIB1voHPT/rOGF7X5LNTYgN7USKypnu1L59O48itK168gVLwGXdN6+jR2S3w4XEaQ9XVI KVm2+Ct46G+9KhydFo0W9Qz2kZunL+P1B/D6A6T2G0DumMkYZ11OqKac8vUrKPv8HXYu/YLQtg0Y vSAgvYEnLZO8STPIO+hoCg6aRWrBIHypPTvZy5+aBqlpQKH5QjvjEoK11dSXbKF86XxKv/6A2hUL 0CKhHj3urogPy2pRgg31ACxb/BXKI/fx/Z/8oldiHMnNPXHEotdQVIWU3AKG5RYw9NDjCNVUULpy IVs/eInSBf9HrKmht7vY4yhuDxmjJzP0uHMYPPN4/IWDcO3lSuwpmdmkZGZTOG4a486+jKadxWz6 v7kUf/IWjZtXw16I77UWDiEEyxd/zctPPcilP7+FwF4eVel0PY07rr+c5QvnU1qyjXBTsNvR8jlP fUHWqIndasMBjFiM6k0r2fDWv9k273UiNRW93aVu4/KnUjjzBAaeeAH9J8/An7bvVf4O1lRS+e3X rJv7CBVLP8OI7Xn3xS6w5PF4Sc/MonDQUE4+/Ty+e+HlezWPY68tltQGx2jpERSXi9zRk8m57h5G n3kZ615/guL3X0arr+7triWNy59K/yNOZszZPyFnzGRc7n13fZeUrFxSjjyF/ofMpmzZAla/8AAV Sz/F0KJ77JiJFkdjfR3lJduZ9983ycrO5tjTzt9rmaO9JxoH8pDJHkAoCtnDijj02rsYdeoPWPn8 /Wyf9wbGXvS/u4ri9pA97SimXHwDueMP2usuSHdw+wIMPPQY8ifPYOeiT1nzwv1ULJu/x4LV8eBo NEJDfS07txfzvzfnMmj4aEZPOmSPHLM1fbfO+gGKUBRyRk7kyJv/xYzbniB7zJTe7tIuSR82hkN+ 9U9m/+FpCibP3K8EIxG3L8DgI05i9t0vMOnKO/DvgUpoNvHh2FCI2uoqijetZ+5/HqV659Y9dsxE ek80HPdkjyJcLobPmsNRd7/M6POvRvWn9naXWqD6Uhh1xmUcdfcrjDrpPHwp+1b/uoonkMbE869k 1l9eZdDs7yJce2bZCMOaWh8ONVFTVcHqFd/w9sv/QYuE98jxEuk90XDck71CSm4+h/zsD8y4/Umy Rkzo7e4AkDZ4JAff8iAHX3sXGf2H9HZ39gh5I8dx2G8fYdJVd+LLyd8jx7BnJoeCjVRXlLHgs3ks +PCdPXKsRBz35ABAqArDDj+RWXe/SP7hJ7dYTWyv9kNRyJtxAkfe+QKjjjkd1b1vLd7U07h9fiae cwWH3/EMGWOn9vh1l1IiDYNoNEJjQx2lJdt4782X2bx2RY8epzWOaBxApBUM4ohbH2Ps93+B4vXv 1WMrXj/jz7+So257nOxeTIHuDQonHcrRf36Rwadc2OPuSmJ8o76mmi0b1vLGs48Taqjp0eMk4sQ0 DjD8qWlMu/y3TLvuL7j20srnrpR0Jv7090y84jZ8+2DOxd4gNa+AGdfdw4hzfobSw6X8pJToujmh sKaynG+XLeb9119C6rEePY6NE9M4ABGKwpjvXMQhtzyMJ3PPrjXqzcxl6g1/Z/yZl/V5d2R3uP0p HHzFbxlz6W9w+VN6tG07vhFsbKCitIRPPniHdd8u6dFj2DiWxgGKEILhR57MjN//B19u4R45hjcr j6k33s+o485CUfetknW9hcvjYfIFV1J0xe9wBXpuxCiev2HVdy3ZtoW3X/4P4cbaHjuGjWNpHMAI IRg07TCm3/5UjwuHNzOXqTf+gxGzTkFRnB87EZfLxfizLqfosltx9eBQeDy+EQ5RV1PF2pXL+eT9 t3qsfRsnEHqAI4Rg8JQZTL/1cVxpPTPxSQ2kMfkXf2X4kacgHMFoF5eqMv6syxj9w1/2aIzDdlOa go1Ulu3kk/ffobR4Q4+1D45oOAAgGHzQ4Rxyy0OogbRutaS4vUz86e8ZOftUx8LYDS6Xi0nn/owR Z/+4x0ZV4m5K2HRTtm/eyEfvzO3RtHZHNBwsBCOOPImp1/wJ1dW1KUlCURh/3k8pOvUHTgyjk7g8 HqZdejPDZp/WY3kcUkp0QyccaqK2upJFCz5n24ZVPdI2OIFQhxYIxp56MUUXXtelG7jfYScz7pJf HfCjJMni9qcw8ao/kT6m40WpkkXa9TcaGyjfWcIn/3urx6wNJxDq0Iaii28kb8YJSe2TNmQUh157 F25f58rtObQkLa+Qg675c4+lnCcGRevrali2+GtKtvRMbMOxNBza4PX6OPxX95PWf2intld8AaZc exfp/ffNZQT3F/pPnsGYi65H9NBMXymlWUU+2EhleSlfffZhj7TrWBoO7ZKSW8CkG+7rVLr56LMu Z9DBya8y79CWsaddzMAjTu6RtuzYRiQSpr62miULPqeuurzb7TqBUIcOGXbo0Yw598rmZQvaIX3E OMaed40T+Owh3D4/RZfchL9fz9TjkIZBLGrHNnawctH8brfpiIbDLhn//evI7KCWq+LyMPnHt5Oa k7eXe9W36TdyAqPOvQrRQ0KsG1bCV201Cz6b1+2A6H4jGgJQAb8qSHXt+i+gClQ65wGpgFdht22m ugRqL7tUCrs+/2TOu7P4UtKYevVdqO0kIOUdcQoDpjtuyZ5g7HcvJmfC9G63Y0+f16JRmhob2LF1 Czu3buxWm/tFIFRgPiyZHkHAJfCpAr8qSHG1/POr9neQ6TE/60irFUyhyPCYD1xHbQasNr0KZLrN bff2RbPPP90tCKjE+xpo1ceAS8TPuyeFY8C0wyk86vQWn7n8KRx0+S37dPHf/RlPII1xF1zbI9mi iSMpNdWVrFuxtFvt7ReBUFsIFCFwCXAL8Cjmn1cReBWBRxF4FPM7lyJQhPmgp7nbPkAK5udeBVQh cClmu4ltxttTwNWqTf9eNjlcCvhVsw+qYlo8butc3Urzf6uCeB9TXD3bx4N/djvehBmxQ0+6gKxB vbumaF+n8JDZ5E05otvt2AHRaDRCU2MD3+63otFJS0NgPrRCCBTrwfBYb9pUl0K6W5DhFqS5BSku BZ8q4g+QKswHLNHisAXDZT1gijDbt9/UaW6FDLcwrQq3QkAVeFVhPZDWQ6nSoQXT06gKpKgi3lfT nbItC4WAquC3+uhK6KNL9KybktqvPyPPvAKEwOVPYcy5VyHU/ca73S9x+wIUnXcVak8UTJKSmKYR agqyo3gzDd1YH2efX8JAsd7+ccFQBLGaMoQWwq2AUE13wVyoGDwZebh8KUR00AyJROBRJC6PQNPN t7aaIBgexRQMTzRMpKYUYVk0ErM9zQDD7ceTlU/UkEhASEh1CxpiEtEJ8ZNAMktLKdbl8VhukWIJ gAK4FYESbkSrqwBVIIRANyRR4cKXNxAMSQzi1ki0BxcAG33m5VR//haFR8whc8DQnmvYoUPyp8wk d+JMyhZ91K12El2U2qpKNq1bzeRDuxbA7j3R6CQi4d8uYb5hn//9Nayc93a7288851LOv+3vNAgX TTGJYYBEgJR4VLMdAXHB8KsKaS7JB/ffwbuP/q3dNicedyrn3vschlDQpTStHiQZlusT1z/R7ApJ iC9jqUtTfGJSEjPaioiCKY5ea2jTFkm7PYEtAub5r/30HZ696dIWfcwbMpLr3llOLAa6vmcy51Ky cph6+W/ImDrLsTL2Em5fgNFnXkb5kk+6PephSDMgGmxsYMemNUw+dFaX2tkvfnnzITUfHJ9q+u8d sfit51g1710y3MRNdlU0i4UAVCuG4VME6W6o+vYr3n/6Xx226bECjS7RbAWY/RHNMRHFdIvsP4/1 kLsUYbpT1ghHhscMaKaoZgAzxQpwZrjMbQIuc/sW7SmmheVTBWluSHO3/dmEMIOkLqtvyh4qHpw9 8yTUHq465bBr8ibOIG1Y21Xuk0FKiWEYxGIa4aYgq1eu7HJb+4VoQLN14LPfwh0QDYd5577boLrU Cnaa8QxV2HEB8//diiDFLfCEG/n3b68mFo102KbLOq6qCCtmQKvgqTWaoTY/+H4r7uJVBR5LRMx9 zPiIVzUF0NcqwOlJEIiA2txuiluQ4YZsj0KGp+0FEJhulqrsMher+/RSJfMDmUB2LgNnzel+Q1Za eSQSprKilKb6rhUf3ucDoTYi4WHd3Vt0+/o1zL3vDjJUg4BL4FKbg6iK9eZOcQnSVMmHj9zF1rW7 Vl1VWIFGpVks3FYsJMVlBk6zPIIcryDPK8i1/rK9ZkA1za3gd5kCZrfRQsQsi8KrCgKqGdzN8pj7 5/kEeT6Ffl5BP59CjkdpNwgrbGFynuk+yfDZp3d7MmBiScD62ho2b1rfpXb2+UBoy13MB6wzL7v5 rz/HpKNOpOi409CkREowpLmvV1VIc8HO5V/xxuMP7LYtxX6oBcQUM7ZixhfAK2OE6qopW7+C6q0b KV6zEl3XycjOYdDYieQNH0PGgCFIXzphHZp0iBrNsQ3TXTJHRAKqwKfoaJU7KVu3gq2rllNRsg2P x8vIceMZOGI0g8ZOIGa0XaHcDIHYDpgzG7CvkTJwOJlF06hY+nm32onHNRrqKSveyPgpySeQ7Zer xrenGUJRkEZzeDEaCfPqvb/h19MOIT2jAEOCbpgPaIYblHADT/32mjZuSet2bDwq+AzAHnUxYmxf /DkfvPAEKz/+H+FgQ4f9HTx6HDPPuJDpp19EZmYewZggokt0aQqSVxWkqpJw6RZef+I+vnr9OZoa 228vt6A/WXn9On1dHPoGbp+fftNmdUs0EuMaoaYg61at4JjvXpB0O/tFcldnmHbkMWT1K2jx2faN 63nxb38gyyUtN8HM6chQJR89fDdb1nzbYnuP18fUI2a321W/5YpkewTuujJeuu1q7vnhaSx+55Vd CgbA1nWrePGuW7j7/KPZ8slbZLgMM6/ELUhzCTJdsO3z97jne8cz75mHOxQMgMrSEtav+KbT18Wh 71Aw/dh20/mTwo5rhEMUb95ItGnX92577DeB0N0xeuohnH3VL1GUlh7//NefY+mHb1PgV8n3KeT7 FEpXtOOWCMF3r7iOooNmtGlbYo60ZHsU1PoK/nH5GXz68tPoSS5GU1a8iYevv5Tlbz9PjhtyPWYM pG79Mh7/9c+oLt2R7Gk7HEBkDBiOv5tZuHaNjWgkTEN9Ldu2bk66jT4jGqqAOd+7lImzjm/xuRaJ 8PLdt2DUlNLPp6CG63n4N9egtXJLRkyYxPevvbHD9hUBXqHz6B9vYcO3y9p87/Z4ySoYwEFHHcf0 Y09ixITJBNLS2pTNCweDvPinm6hdvZACn0qaKpn799upq2xb58AXCDBi/CSmH3sSRQcfSiAtHUXp Mz+ZQ5IEsnLIHzm+2+0Y0iAaMVPKd3ahmtc+n9yVDIFAgCt/9yd++e0SahMewu2bNvDQnb/jprsf 4JX772LL6lZuic/Hz+64H99uFq9Z9uWnLHjzBaRsGZAZMX4SF954O0VHnoDH5UKXpojV7djEy089 xrxnHiLcFIxvX19Tzav/vIebH36GdYsWsurLT1oeSAgmHHY0l/7mT4wYPxUD00WqL9vBe6+/zDv/ upv6mqouXSOH/Zu08TPgfy92ef/WcY3ijWtJdnbLfjPk2lkGj57AOdfc3OaNPP/153j0zt/y2mMt 3RIhBKdffh0TDzHdknaDrNa/P3j7DWJay5GLwaPG8vunXmPi7FOo1xW2NulsbdIpCem48ofxk9/8 iXN+cStqqxL1y776nO3FW1m38DMioVCL7/oPHc719z7CwKKplIYNtgZ1doR0RE5/LvzJdfzwzgeT vzAOfYKBk6Z3v+CRnVIeCbN+3bp45nJn6TOBUICYAWFdcuL5lzDhyFZuSjTKaw//rR23ZDIXXHMj wZgkupss3Q1ff9Lms1N/dDWp+QOpjBrURCW1UUldVFITheqoQVCXfOeiH5HTf2CL/RrrainevIm1 Sxe1aXPWnDPJHTCYWs2gNiqp1SS1UaiJGARjBmmFQzp5RRz6GmpaJp7c7lf10nUdLRIhWF9LY01Z Uvv2GUtDAo0xSU3UoEn1ccWtd5GZu+sJOV6fn8v+8A8a1QC1UYPGmNFut+zPStsJGg0fPYaYhJgh iRqgSXOuSUxKNAOiOqSnpeP1tK07IYPVVJe3/cH6DRxktmFIYtZQcUxKs22cLIwDGVdKBt6cgt1v uAvswjyxmEZTU5CdJSVJ7d9nLA1DmlZGY0xSGzXwDh7DGVff0uH6HUIITrrsWvInTKc2atCgSUK6 +cB3xMARY9p8tmb5N+ZcE2tGqp2V6VYEbsVMF6+uriIcaZumrmbkkz9wUJvPN3y7HJeQVl0PM4/D ng7vVfZ+ESCHfQd/WjqZ+d23NAxpENM0wqEmKiuTmybfp+6/qCEJ69CoQ70mOeTMiyk6sv31O4ZP mMwpV9xATdQgGIOoBF1KFEXF5fHG/8w8VJNRh85u084bj95HxZZ15HoUsr1m+neWV5DtgVyvgk+R vPro/VSVbGuxX2ZOHsOGDWP4+Elt2vzi3ddYuewbMy3dZ7ab4xXkegQedHas3v/zNATECxq19+dR nGS1jvDndV80pJRxFyVUXZrUvvudaEjaN88lpukekxDRJY2aJKh4OeeXd5CWlYPqcqO63Hh8flIy sjjjprsJuwOEDXM/MOeYBNIyyMgrIC07D39aOqnZuXgCKQjg5O+egcfra3Hc8h3buPmCU/jqrRfx aU0M8EgGeCR5Lp26DSu45+dX8Pq/7sbQWwZMph4xm8IBgxh3+HH4U1pvlZHxAAAgAElEQVSO2tRV V3HPFWfx/lP/JFpbRT9VwxNuYNNXH3HXVRfzn99e3eG12R8IqKa4plrlClNdZuJdmrv5szSXuc3e rpK2P+DO7976MlJKaxRFR9OiVFVVJrX/fjXkKpEYUrQZ8gTTStANiWaYvr+UoGsS79Dx/OL1Rfgj 9eabSwCKQmDACJr05oLFXsuV6JeVxrjhg2kIm6MkwWCQ7OwcXIpg3EEzOPHSq/ng6QeJRsIYVnJX +Y5t3H3VD3B7vQweOQZVdVFdUUblzvaTtbLzCzjryl+iqR5GTj2UyUedyIJ357bYprJkO/+69Xq4 9fpOXhswpETu49IRSCjdKIQ9jb/ZqpDCdDUNIZASAqo5Qye0h2qE7I+4M3J6pB17+LW+ri6p/fab uSeGNeFMMyRGO/vqhlmlSjdAN8x5HYYAIwberHyEko/bKtOnWO15LTvLnoaeqirk5/WjqMisXdDU 1EQ4HGbkgHwkphVz6XU3U128nm+++BQtGiYaDsWHrLRIhI0rl+/yPFIzMvnB7x/AN2wCO0M6LkVw 5vW3sX7pV1Tt3J7cRUlASuv8E66NWxFE27tYvYQ99T+xdKNLEbgwyw5guYgxK5Acw3QOvYo5stVz 657v37jSe0A0LGtDSkldY1Nyx+/+0btIklanxHwgwh0EKyPW6IWOKQj2Qx5XGCEQ1oQ1tyrwClCR NNZW0lCymdKaKratW8WadesZniLITE8jKyuL1NRUMoaPBSGIxsDwpnDVvU/w5pMP8N7TD6GoLrRw CD3WduZpy/MVDBxVxA9/fz8FU2ZSGZHEpMQlINB/BD/914s8dsNllG5cndyFiV8fSciQ6EbzsLvb Km24L7yk7fiFalsXmKLmVyQ+q6QjmHGpkC4JSwVpdVwVZl3XBk06wgHk9eve6EkLpETb3b3biv3C 0pBSIoVAl5KwIUwxaEVYh5AuiRkSA/PBsQdOFMwRjYAKKYpky6JP+PTNl1n20TvtDnl+0er/Dzro IKZv3Mrs08/FlZEHvgCn//SXzDj9Qt5/5hEWvv4sVeVlHQrH6KmHMPvCnzDlpLMJK26qo6bIGVKi CIgYgqwxU/n13E/5+D8P8unzj1JTXopoJ2V81PhJ9Bs3jU+efbjVNRKEdFMoTeGUCCC9VTX2NqNJ sqVDE0p4KjUpcbW3TytsCwKsd4G9fULbiccQwqzV6lUED11wJNtXLmnR3ojps7j4sf+a1qVhuicK kjS3INKDNU+BeG0Us1/N52m7wGaZRvMzzSrXqCrg6WYxooguk6obuyfRo9Gktt+vLA07eeuCf7yE P6HsX1RCKGaKht5sWMQrdPldggyXZOeSz3npnt+ydunXSR178eLFLF68mH/fcxtnXHY1x/70V3jd bjLzB3De9bdz9i9up2bTKsqKN7Bj03oM3SA1M4vBo8aSM2wsrowcQjFJvQ5hTRK13ScJQpjulpTg d6VwzOU3cPJPbqR+2wbKN66mZPMGsrIyKRw2mn5DRpKWVwjAT+64D01CVJeEdUkwBsGYRLdudiEE ipRm7MC63u1ecuvml9Y/UlyWSAN+aw8R37Tj4d52n6GEts16JmZ/7HKEXqX9SLxilXXUDNPFNNsw T8DThdC9fU1aCFfi+bR3bYRAYoqK+RIyi1Qn7g9tr4m97e7wqwJdQoPW++KxWyu5Fft8IDRmQMQA n1VfRtNNPyyq2xW8zAdQt5Kp7B9AwXRDUlyCDBHjowfv4pX7/titvgQb6nnmb3/kkzdf5qbH55I2 aBRB3XxQvUOKGDa0iJGzzW0ldpxF0hC1Er0ssZBWoRxFWA8TkqhhVkDXEQSkJDBwJKMHjWTcMebt aUhz3+qoEd9HNyAGaLoZz7DPXWDVRbVuaLvqmS0g9h9YNznNwqUnBCFlQnuJQUsbWyhEwjYCqzo8 zcFZAzAM0K0d7GUj3B2UJlQE+BXQrJETRcp4AaVksM9BkSIuXHbhIyGar1PitYnva10Tw7om0rom 9jygjq6Jec6i5bVL+C7+J0EgSXcL6ntZOFpPcdgd+7xogJkZKRXzYYsJK0MSibDWDzAkcZcE7LKA ZqQ+XWi8f//tvP5w+5XGu8K2jeu44wencvNTbxAYNBpNN/tju0E2hrTcBatfLiFQhMQtwUC06Ld9 E9thGNslo8X7zdzGQMb3UaR10yugSjAUgTAkCFAQ1kJPZt1Rl1Vf1S6ODObxdSniWadRw3zD69aD Kml+sFyiuQ17JFRYeSyKSPiz2tYB3RBxwdQSrA1VmIWT29MBBfC5BFFrW/v6qaLzK8cZmPeNIc0s 3Zi1/ISkWSRs4XJbRZyVhNwQXZr9jhmm4GvSDLAbdhuY19Wu+WoLhy28iaJhY8flYtKOzVi/r/3D 9xKKKzkZ2C9iGmEDiEkCLnPNESFEXPGRrZYDsG5ojxCkuGDRa8/zxsN/a3dSjhCCsdOmM2rGbCZM nkr+oKEoLpWG6iq2rV3J8hUrWPb+6zTW1bbZt3TrFu67+gf8+sWP8HkDRHVpLtCkND8MEqyHUCbE Y83bSLcEJWY9oFgPkU8Bv0ug1ZRTsnktW1avIBwMkp6Ty7CiCWQNH0dOWpppWWkaaxbOJ2PEBNIz s2jC9PljYJnyZklCnwJaQy2Rih00VJZRWbKN2spyfIE0Bo0cjTctk/ScXPyFQ4jokqaYGSPSZPMD 4lXNtvyqGYtQhVnE2DoUoWCQmuJ1bFy9kuoyc6i5YMhwCkYUkTF4NNLrMpPorDKHbsWMDdz75ucE Wt2FOlAXE5a1JhFCia9509m0jZg10haKmZm+YYN4iUW7rYDLdBNCtdXUbllL6ZYN1JTvxO0LMHzC FDKGjiUrO5eQdU0iuh3fMMXMo9jLZJq/uyBhhI/mqvVCNL9Awrr5FzGaX3h+VdDYXqCuAyrKk0vG 2iVC4EmysM9+E9MIG6BFJR61pSwrmKX47P8WVrFen0ug1Vfzzr/ubNfHHD1pKj/+69MMGj6SoKEQ 1pvjARmDBP2mHcVhF4L84/189MS9/Pvu32O0KgO4aeUyvpz7NCdffCVRl+TaI8ZQU76zxTazTjuX a//2OGFdsmXVNyz5+H1KN6yirGQHW1ev4IRLr+WUK39lmsp6lDWffcC7T/yD1Qu/REoDQzffbUII FEVBKCojJ0zm4OO/gwik8+wfbsDj8zPtuDkcet6PGTB5Broi0GMaa//vTdZ8+CbrFs2noaoCaRjW MJv5b7N0ofl6FUKQnV/IhOO+y7HnXkz2iPGEdPOmVgR88dR9vPP337W5jpfcdi/fzHuP5V98TCwa xTD0+PUWioKiKKRmZDH9lLM49kfXkpk/BE1ay1EogpvPPprNK8xAqO06TJw5i9898w6blyzgtgva z+hNhkBWDje9tRh/aiZgxkVcGCx95yUWvPwEa5d8hWEY8esDAkU1+z566nQOO+9yJp9yDlHVFGUp Tatn0YuP8Pqff9nmeA9+toq8/oMor65mxacfsG7RfCq2baF0WzGarnPpY+/izilE6jLuYidDrL4H yiIIc6EtIQQZqckVLN4v3BMbHdpN8olJCLhE3Id3C/ONuPyD1yjfXtxm+yknncVNDzxNBBclEYMm 3TDjIVYAUMEu9gtpbhdzfnwTBVOO4p7vn4Deamr8h88/wakXXExqShpS14hpLSPRkaYGPn7hcV55 5O+UbNmEHmtZ7SvdJenvU6mrq+HJ237BR6+90CZ7FJrTftF11ixdyJpvFqEIgWEYhIONzH/jRRa8 PZepc87lorseAwmL33mZb//vzY4vqJQYsvlYFTu2Me/pB/j02Yc589pbOOEnN1nDwgKfMNqcG8Bj v7223ZqqANIw0A2DuqoKPvjPQyx5/00uveshxh1xArqU+F0KRiyK1qpdPaZZ82xku8dMFqlFyXPp uDzmC2Xnum956PpL2Lp2ZQd9N6eOG7rOqq+/YPWiLxn6xN+44q9Pkzt8DFHdzC8JdNC/0rUreO7O X/HlB+8RCYdaHCNv8DB8KmZ8rotuiVbXM7VU7BdRekZGUvv1iVmuEcsPB2vURBEosSjL5r3bZtvM vAKuv/PvhFGpiBhURyT1mqQpZtAUM83ZYMygUTOo1SRVEUl5VHLIzMM448e/aDP8uGXtSjZt29bh Wixf/u9t7vvVVWzbsK6NYNj9NSJBHv71lXw497l2BaNdrGy+RFxuN0edfr6Ziu3zcOyZ3+tcW63Q Yxqv/PX3LH7hQXNOjUch0MHaCB0JRntUlZXw799cRV3pNjLcCqkugdKBySlEDy7HoLrwZOSS61Oo WLWYv1xyKsWrV3S679Iw2LxyGX+5+BTq1q8g36eS51VI6cBXuu1H5zLvjVcINwXbPYYdA+nq6Wll W7u4p4ltYSiKitvtITs7uWSx/cY92R22aWtH5kUsTE07GZYzT78AX1Y/doR0GjRzuDJGc0KY3TUh zag90h4eNDj+oh/z5mP/MLNA4weWVKxZBkXj2u2Xsbul9CT899UX+OztV9t1o9weDwOGjcTl9lBZ tpPaio5rH6Tn5jFt5uEYihkPmDTzSAoGD6WiZDuTZx5J1uBR5GSkMWTMOHx+P00N9axavZqytStY 8lnLtUKlNHj+b3/kxLMvIiUjc5cLVNn4Ayn0HzocoShU7NxBXTtzGsq2b+WTZx7iit/eudv4RBJu /i6RmJZBfX0D91//I6rKWrqQaZlZHDbnDGbPOZ20rBwa62pZtHAh8555uMW2VaUl/OOai7jnpQ/I KCiko8qLuxvCtEdPukqoIrmp7O0hhEBVVdxeL4GcwqT23a/ck11hCwaYb6lIYwNb1rRdBGngoMGE dDM4plmpynZUvHV7EoFmSCJW8Kqw/4B2qyatXLSAo89IvhQ8QEMwyKdPP9zmjeT1+/nO5T/ntB/+ lPT0DHM0QDfYvn4Vbz7yd7547402prHb5SYrLY1GzbwjfTm53PDg8+TlF6AZBrXbt7Bj41oaqquJ pqQwaPhIxh1yOOkDhrF93Qr+fPm5lG1vno1bV13FplXLmTiz4zU/3R4vR552LidfcDGFYyfjcXtQ FZC6zudvvsjDt91AKBhssc/qrz4h1FBLanomHZmcugHZQ0dxzf3PABKvapYaSNQZXcLyBZ/z7tMP ddg/RVUpOuwYAi7Ba889xtb1a1p8P2jkaH7/1GsUDh3RotrblCOP55wfXMrvrvwRaz7/IP75tg3r eOaBe7jq9ns6POaukNIMqKqSdqdD7I5QQz21Zd0XDUUouNxufP4AubupO9OaPiMabWjHfIeu+2N2 rKM9dmtNAOlZ2RTNnM3wsRMYd/B0UvMKcecMor5kE/PmPtNm+xO/dxmX3HArYalQFTWISTPiP2zi wfziH0+R+9c7mPuPO1vsIzCHAVMsuz5YuZPVX3zII+++TvG61RixGDFNQ0oDIRRcbheKqqJ6vEyf fTzpA4e3EA2AlYu/2qVo3PDwC8w4+gQaDJU6zUDTwSPNpSKOP/8Stm3fwdz77mixT3nJDhprayzR aN/ciBgSJT2HcSeehRDmCEOWW8FraXZZdS2v/u02PnjhqQ77VjhsJBfeeDtHnnw6SjTEkvffaLON PyWVVx76a4dtZHhVhGg5SfLT157jgutv63Afm6Jph9B/3EFMmj6DjMLBiLQsQsKNmplL1HpRtV/2 qWNiwToiVd0bPRFCIBQFl8tNIJBCYf/kptrvF0OunW3OVm4pwZOaxpDRRRSvazmXY2d5OT5F4FXB HRPEsIYHWsm+sMbfXUrzAtBVlZXtxhyKphzSYb9SMzI56+qbOebsi3CnZhKWCjqmC5XhVqhdVUFt RdtK5IcePwdFUQlGzFiLJiGmSLy6Qo7Xzdix7btDtnB88tEHPPWbqyjdWoyUbcVTWittARAK8fEb L7fbXqixvsNzA5h68KHUGyqVEbOQUcwAt2rmZahCYcSotiX3q8tLCTbY7XZgaUhruFo38xjcgOEC XdP49L9v8u+7b6Nk07oOXDovx37vR5zz0xtIzR+ArkC4vobVS9pmAq9btoR1y5a0+XxX1NdUs3np /A6/nzL7RL5/420MHDWOsOKhUTfrvIQN8OhWRrBh3XISwkmYHHpDLdHKnbvfcDcoioLb6yUlI4vU rPyk9u0zMQ1o9hVjEnD7yCoY0EY0Frz+HN/76XVkeNLQrOc/YkhcimjhoqjCHFbzqYJ0F2S5Fd58 6Qm0SLjNcfPHti2kYzNt1vGc++NrqdGgPGLQpJuJWR4FYtLAnZqJPzWNpoaWD2e4sdEM6gpzJMcw 7FXqd38dln41n7//7HuEGpKb8twVooakLmpQr5kTzQxrroaCoCEm2/fdpZ1mBR3eCFZsSlfM8/a7 BKVb1/Ofu29nwXuvdziqMnLiVC7+1R8YPfMYQoZCUDPwqoJITHY+yNwJVi1aQFZu+yvd/fxPfyd9 4HAqwga1YUnYShAzyzc0T6o0rGzmJGLJbF/+dbwkQ5cRAtXlwuv1MWrUqKRTbfuce2LYmXteLxOO OJ5vPv2/Ft9X7NjKP+/4DdfceT9g4FIgpIt49qAdUFUVM48gxWUuaLRmzSpefuDuNm+2/kNHMGzo 8A77o7pUmnRBVUSnTjPjI5LmeTPevP6kZ+e0EY23n36QkdMPJycrB1WYiVZeRZCiClYv/ZqXH/57 u8eLRKLMfeS+NoKhqCoTDz2cwWMnUTRlGgC1VVVs2rie6i3rWbHwy3YFcXdE9ObRq5j99rRmHNsJ Yl3BKwQBl4JXSohG+N+zT/PWA3dSXVbarnXhCwQ479pfc/LFPyMlJZXaqEFUNmfTpnhUfIGUFktJ dIdQY32HooHLTXXELDTdGDMtC/veslPRpTUtINm5Jw0rF3Sr3/Ywq8tlxjOGtFPCcnf0GfckrJtT rA0h4pl3E085h8xH7m2xBoqUki9efJxI+VYu+eO/GJjfn/qYJKKbs2jt2IXHqrHhUmDB3Kd4+Pab CLWzXOLR5/yAlPT0jk9TQpMuadLtB8zKBBSg6pCZXcjYGUdRWry5xX7ffDGPG+fM5LCTv0v+4JG4 VahuDLFlyecs+eTDDm/+YF0Vm5d+2ebz71z8E6645U+4/f4W73ZDgh4JUVrTwL9vu5ZP3prbZt9d YafJ2+nVBs2p8Ynp1MniUSFDCL5dtoR/33Urq+fPa3fIWlFUJh02i0tv/Quji8ahqCqaYaaGS8yX qE8VuP0pjJo4hRVftZzDPGHGEfzywRfM3IldENahZP0qho0pQgiB1+fn/Zf+0+62UV0SjpmWV9Sy MuwcILDmCUlzpmsytk9TbRVlG9oG95NFEQoer5eUtHQKh4xMev++457IRPfE9CEzcwo48YrrefFP N7XY1DB0vv7wf6xccghTZ85i5OHHMXL0GLLzC804Qn0t24s3sXb1Kha/+zKlxZvarHcCMGD4KE74 /hVohohPr26NORlNxtPFE4d2I9bb+MhLfsGyzz6kJqGOqDQMyrdt4fVH7kvqMsSiGo31bd2S3H75 ePz+Np8rAhSfn+xAuP2SA7t5DcY9jXg8ScZniMa/7wJaOMQLD9zNa4//i8a6mna3URSVI087i+PP /QF6Uz2rFze/hXXZXLTHBahuF+OOPKGNaKxa+CWP33Y937/uZgaMGtfG/ausrmHxZx/x/tMPsm3D Gu7/7wLyWy1H0ZqoNP/igmoVFmqIyTbTHpKhbvsmQtvWd3FvE2G5Jh6vj7T0TAYNGZZ0G33GPdEx FVyoEiHNilVBXXD4uZdSsWEVH730dKs9JMGaKj5/9zU+f/c1vH4/bisH39B1IuFQu282m6x+BVx5 zyOIlExqogYZ7vZV0FyKwJpkhi1s5jRr3TAtkOwhI7n4d/fy9K3XUlPWvSBXSkYGQ8eMZ+03LddT eenBv9Ko6Uw95FDyBwzC5XZTX13Ntk0bWLNmNV++/hyV7eS1rFv4BcFox9fBtjB29X377PqtEW5q 5PN3X+9QMMCsqP3lf9/kq/ff3mVbACnZudzy2pd8/uoz7NjU/OAZus4nb7zIoo/eI3/wMEZNmERK eiaxaJjN69awZd0amupq47kXLzz0d6669S4UV8emScww4xRx4cRcXqO7RdRKv/4QPdq2qn1S2PEM n58hw4bjCaQl3USfEQ0wU8w9VkBTMyAUk7hcKZx+8z243B7ef/bRDveNhEJtVjrriNz+g7jyvqfJ mnAoNVGJKsxJVe3dFFKaE5gSK2rZZnxMmoHEYEwwctYcfv3sGN546K8seudlwqFdl2Dzp6QSCja2 +EzTY2ioHHP2RW1Eo7GulpfuvZ3XfT48Xh9CmPNTwqHQLgOEqxbO55Hf/pyswvbfrpphJcclnrP1 /3aco31kq393ASmJhjsXh3H5w+RlZ3PDXx/mD1dc0Kb4UrChnk0rl7FpZdt1ehN5/98Pc8y5lzBy 7PgOraioNWnQ/r51GcauoEVClC/5tFttJMYz/IEURo/rOIC/K/pMTAOah+m8ikQXprXRGAPcqZz6 q7sYOW0Gbz/8F7au61pJPV8ghSNOO4/jL7sO74AR1GhmEpJZF6L9EfeYNN2QxDeybW0YCLNeiC5p UgR5Q0fzi7v/RclVN7Lov6+x+puFNFaW01hfixAKaZlZ+PMGMH3mYXjyBvLXK85ucaxQfR1VW9Zz 0nk/oGTjWt555vE2owzRcLjTDxpAXkF/TrngYr754pN2vw/p1rTzVhXApJREpSDawdNiWPVc98gw WntIA6mFGT7tMH792Fzu/+11FC9bmHQzuQMGsWPnTvJGFMXjU60J6xLZKo7RXYLbNlK7Ormh4fZQ hILb4yElLZ38ISO61EbfiWlYhGISl1s0T880JDIGMdXHmFMuoGjWiaz8+F0WvPcGm5Z+SX1N9W7b HDlpGiOnz+KI0y8ga9hYgripjcr41PGYYdaVaO8e0gwzLmAHWe3IvwFmAppVL0KzxGXH9i0QqmfO j67hHLdKTIsQ0zQEApfbjeLx4XG7WPl166KEEKyvo3jDWgaOm8r5N/+ZsUedwrtPPsDGxfNpaieI ayOEYNDI0RQdNJOU/AG899h9hIKN5A8exm+eeo384UVEPv243X2bdMN6QBLyZDAFXNNlhzkItZqk MmoQa7eyPIRjPftekYZOTX2QJsVHRtFB/PKpt1j69kt8+fZLrF26kNguzP6U9AyGH3QYBx87h6Lj TsObkUtt1CDYQZ57SJe4DGlVK+uZ89j08Rto4eQKALdGCIGiqni8PtIzsxg2LPkgKIBYVNq5UNUd 11/O8oXzKS3ZRrgp2G62ZTLMefILskZP7FYbHeG1siLNoVNr5qtViMZv1YVw6Rqx+moqykoIl2yi vqqSzWtXEQ2H6D9sJJk5eWQPGExg4Egys7LAnx6vQxppVSPDrs8QqdyBFx2XFQiMSol0BxAZuaYZ L1umrJuFXMyFgVJdCqmxJh7/+YVsXPIlKVk5jJk4lf6ji/AXDqOfVTpdM6B851a+fOtlNq1uGUn3 eH3c9OgrDJt5LE3W3Sq1KMHqcmq3rKVm53bqqyvYsXE9/YePIiUtg6zCAWQPHEp2fn8CaRkYQmH7 2hUsnvc+U4/7DtlDR6NLiDTWE66vjQ/pa4YpCL68gUSlEl+O0pAynk/iUUCJNqHXV1ml/awaGVKS mleI4vYQrNyJ1KJWYR6r3oXbR2p2Ho1VpRDTUBVQMet3dPZdExcuwwxER6VCILc/UjFjES5hLlmh Rpuor66idv1yKrZvYeO33yAQ9B8+krzCgWQW9CdjyBjc6TnEXB4iloUkBOhNDWgN1bisgkcxKYkY Ek/uQDShWLkZ5j3TkcB0hqbqSv7v56dRv/HbLrcB5rC7P5BCfuFADp11HNffkVyQ3abPWRpgvrGJ SXOhHcOsgCUNuySgpCkmcCtuPOn5ZGXm4xo7FVXAFGF2TFqjHDrmQ1onQYvIeAWo+BCjtCtpgTQk 7twB8TJ7AC5p7qMl1ATd1a2zZN67rPziQ/RYjGBDPeVbN8M7nT/vzIL+ZI6aSLUm41XbFTy4cwaS mzeQgoSKW9CyanuDhLoIgEQdMoGDL56AAVRFTAtJeNIQeelx0VClxGuYwqjZ52ZZU2ZRY7PyldsT wJ2XgqI0lxoEaDTMIQYloyC+BgrW9zGgJgYiszBew1PSXC+0s0jrNxQGKIakUYIRM+L1QV0x8KgB 3HkB+hcMYhBwsF2By7o+ZsUuCFn1XOxYhQAUbyouX2q8VKAKeAyJZpeetK5JuJsBjYoVC2jY3DWX 2qZFfkYghaLx47vcVp8KhCYSsUq1+V0CjyLjC/DoAjTMN4Jir79h7aNYpcDsoVtJc2Vvw2jOQbBv BhsFYQYDdYkmwB5wtN92tsDYYqRZqdaJNFZXMu/xv+1yxGZXeHx+TrniemRGHo2atZyD1UfVeijt QwrRNijbeohUIltsI6z/Sig0Hn8oDdnSf7fbRJqxi5iQCKPVvnZfsK5XoiBY39s1R+0+dwW7oLFd WjHxIBpm3owqBEqsucqWXYELmn9vvZ02BLYIy/g1bXFNsHJVuqEZWriJda8+huzE/KbdEY9npKYx cERRl9vps6IB5o/XGDPL8Jnrg9oFaoU59d2qMZr4BkzEvkHst44tFJJWFoM9hIpZs9O+wZuzAGW8 9J9dMT0FgT1h1t5u3HHfJZCeQcWWDZ1eOCmQlsGoqYcw63tXMOTwk2i0p/vL5n7acYP2zrPNubT+ LuEBV4RZMiDxmtDq/CT2miaWhWLFepSEgjOJ7doParudkHTwRXK07quNWbHdFDVoOyEx8drYmZx2 G4llG+1za32cnqg2XvbNl1SuaJuslyx2PMPr85OZk8uwUWO73FafFg0bXVriASDMmo4C06+NF8ht 9SqTND8tEcMOeMp2hxAVpdW6Hy3asdtouW9USnwJxYXd6Tkc9qNfMvviqxBN9TRUVxIuLaa+vISK Hdso29ZceCWnoD/9Bg8lY8BQcoaPJSU7F80VoDFmCoYdX7Af5myitTUAABOwSURBVMQ+tVjbI+Ec oWURZI9iisCusHe1r4v9LhQ01xUVQrYryPb+e2nspAPpkbgTXbYEywZaCox9D7RusaPf2y5I3B3B 0MJNrH7xn+iRzqUC7BJhBtL9gRQGDBlGWlZy0+ET6VNDrrtDt45rB6USq3LvqkO7Mwx1KzCaDLZY 2DERe39d9ePJCJCRVUDuqAnNJfYR8WrFtqmsWa5OnSHRNBkPtsZdIazU9RY+dcf9jLsVmK5MVw1i 261r2heWdtsNLe8B6Oj69MbKbjsXfkzFN591ux0hBKqi4vF4CaSmMWHi1G611ycDoZ2le+M/3UOX ENXBp5prnUhhBlPtXJPWsRZ7uQawXaWW/rY9z8O2MKLSzBfQuniSB8ryh715D+yKaFMDq56/D6O7 GaC0dE2ysnMZvd+Kxr7/EtrjNOlmLMSrWmP6Vs5Ge7GWuFeR4FHYN3xi7MS2LkL6vr5+vMOuWPPG 01R9m9xKgO1hF9xxezwEUtMYMHgohYO7ltRlc0BbGr2NxBSOiG7GRexV7BXRPHUee4GgVgqQ6G/b Q54RwxzlOVCshL5K+YaVrH/pn8geqv+hKqaVkZGZzYwjj0You5nSuxsOiEDovo65EllzXEOQuNTf 7u2Fnso67CrSkIjOVB522C1aOMTqJ/9MqHxHj7QnFAWXNczar3AA4w86rNtt9t4SBg4dYscmOvvX 225I7ZKPadrSveQjB5M1bz7N9i/e65G27ACo1+sjLSOLKdMPIyOng8JBSeCIhkO3CdVWsvKfv6ah rHO5JQ7tU7J8AWufuRfZAwtEQXPtDH9KKnn5hcw86vgeabdPLJbk0LtUlG5n7fwPWfDXG7s9qepA paFiJ4vv+xXhqo7XtUmGxBGTtIxMJh80nf5DuzZBrTW9JxqOC9xnqCkpBqB8/nusevLPbZaudNg1 WijI8kf/QP3apT3WplAU3G4zlpFfOIBZJ3yn2wFQG8fScOgxpGGw8sUHWf3Wv3u08ndfJqZFWfLE n9nyvxfbLZjcFexYhs8fIDM7l4NnHMHgUV2foNYax9Jw6FEMLcK3D93Kho/fxuhufbs+TiwWY/mL D7LxlYeQu1nKsbPYbonb6yUtI5OBw0ZwzJyzeszKACcQ6rAHiAUbWPbX69j0+XtIRzjaJabrrHz1 cdY9dVePZH3amGu0ukhJTSM3v5CjTphDQRcqju8KRzQc9giR2kqW3n01Gz9717E4WhGLxVj12pOs fvR2YqHG3e/QSWwrw+cPkJGVw+hxEznqhFN7rH0bJ7nLYY8Rqalg6d1Xo0fCjDr29HYXzz7QiEWj LH/5QdY9eVePC4ZQFDwe0y3pP2go3zn3B/hSM3vsGDbO3BOHPUqktpKlf7mWSE0548/4EarH09td 6jW0UJDFj/+ZTXMf6lGXBEzRcLvNgsF5Bf2ZddzJjJkwrUePYePMPXHY48SC9Xz74K3o9dUUnX8V vrSM3u7SXidYWcaSR25n6/9e7LGgp40dx/AHUsjK7ceEyQdx4hnnI9Q983g7Q64OewVDi7Dq3/fw yW2XUbNlXW93Z6+yc8VXfHTTeWx999k9IhiKquLx+UjPymboyDF898If4U/L6tHjJOIMuTrsNaRh ULHgf3x68/ms/+iNPp8EpoVDrHjlEb645SLq1izpsTwMm8Q4RmpaBgX9B3HyaecwbMyeqfJv4wRC HfY6DVs3sOiPP6H6lO8x9oJryOg/pLe71OOUb1jF6ifvZPvn7/a4dWEjhDBXS0tJJadfAYcecTQz jp2zR46ViCMaDr2CHg6y/tVHKVv6KUXfv5FBR5yCLyW1t7vVbaJNDax98z+se+EfhCpK9thxFEVB VV34/AGycvIomjiFU8/9Pm6vb48d08YRDYdepX7zWhbeeSUbp81i6sU38P/t3VtQW/edB/Cv7neJ qyXuRtgoAdtxHBK7Thtir+ok65L1xpa8nSTbyWyh7T51dnjwPhledvzA0+5LB7ZPndnMQtLpDLvT zlbThmyS1okzXTuALRKEwQbjC+YiQHdpH3SxBMLmDxIX5/uZ8YwHcf7nf4TO9/z+5/x1TnHjC5DL d98VlpB/GXe+/Bg33v833L/6WU4eObCW1JfRNBoUFBWjunYf3nqnFUVl1XlbZzpecqVtFw0F8ODy 7/HHa39C+XffgM3xUxTbntsV4RHyL+Petc8x/P6/4v5fPkY0R19rX0vqxKdSBYOxAJaKarz25lnY Dubn8mo2vORKO0bYt4iJ3/dh6pPfovw7p7D/rR+jYN9BaHbgJdql2Qd4MPg5Rj7swYOrnyCS43kX 2aTu96lQQm80YU95JU68/jdo/utzQA6/W/IkrDRoxwn7FjHxh1/j9v/+F0z1h1H7/XOo+s4paCyV 21p9hAI+LN+ZgMf1IcYH+rE4dh2Ibc39zNMDQ2cwomRPGQ6/cBSnHe9syXmMdGKhsdFn42VtK3dN 0dMpGgpiduhzzA59jsFf/gtKDx1DadNJWI58DwZLNVR6Q977sDT/EN7Jcdy/8gdMffER5oYuIxTw 53296VYFhrkMB4+8CMd7P4PWmL/5GGtZd2gkn8wlkUhyGx5E6xD0zmHy099h8tPfQa7RQWdtgGX/ IRgPHofl2cOQ6wyQ64xQabQbXodv0YuIbxHhpQXc/stnWBq+jDsjX8E3fmPb5pRkC4xDR17C2z/5 OSzVuf326nqtOzTkCiVkMtmqxxdu1OKdm5AolDlrj7ZecrJS2LuwpesN+5YwP/QF5oe+AH7zS0gV Sqj2VEJdWoEicznUpZVQmKsgNxVDaSpCUUnmzXQlsRju351GaGEG4fkZhO6Ow3f/Dmbu3EJw5g4C 924jFt3+xyil7o2xIjDe/dk/oXxv/bb1a92hUV1Tg8Ev/wypRBpPP4lkUzPcBv757Q0vS5QuGgrC N+mBb9KD2e3uTI6kLquq1NAbTSjZEx+SvP2Tn29rYAACoVGz7xko1WrIFQpIJVJE+UgeorxITtxS aTQwGAuwp7wSh184Cud7/wjzJp+OlgvrDo2qvXXQG4yQK5SQSKWbrjSIKFPy/IVcrkjc37MYlopq nHj9TfzA8S4023DSM5t1h0Z5TR0MBUVQqdWQyWTI03R6om+l9ElbGp0ehcWlqNpbhzfOOPDKG2e3 /LLq46w7NGQKFWpqrRi9/hVkcnm82ojFWG0QbULy/GByOKI3mFBUasazBw/j7LutqD9wZEsnbq2H 0DyN+obn8KePXFAolPBL+FAcos1Iv5yq0epgKiyGubwSR7/7KlrO/whFlqrt7mJWQqFhqayBVm+A QqmCVCpDVBJhpUEkKFldSGUyKFVq6PQGFJbsQU2dDafPOHHsr05DrlRtdzfXJBQae611KCwqgUar w6J3HmGe2CASkgwLuVyRemRiqaUcB557AX/7Titq6nP3UKN8EQoNlb4QVdZ6TE6Mwbswh3AwiHAs hugOmAhDtJOln7tQqFTQ6Q0wFRajvGovmk+dxmtnzuflzuH5IPyFtdNnf4iJUTcW5mYRCgYQXY4i xhOiRFmlD0UUCiXUGm28ujCXo77xIN48/yPsa3w+p09Ayzfh0Kiorcfrb/0QszP3EfD7EA6F4pVG NMrgIEpIDwu5XAGlSg29wYiCohJUWPfh5KkfoPm1Fii1xu3uqrANfTX++Kun4Ll+DUuLXoRCQUSj UYRCQQYHPVFyZ8r40mNapbqbPz/p2yaTyiBXxMNCq9PDUFAIS3klXjr+Ck6cPgtzlXW7u7thGwoN qUKFM+/+FLcnxuOhEYmkAoTBQWuRSqWpGY8yuRwymQyxWAzRSAThcBjRaCRetSZCZDd8jtKDIjn9 W6FUpq6KGIwFKDFb8PyLx3DijTOosD6zq4Yi2Wz4JjzGwmI4/v7H8P1iCQAgvTeNJe8CgsEAohFe iqVHUtOjExOYNFo9dHoDtDodgsEgAgE//MvLCAb8CIWC8SFvJIJoLJr6tulOqERW3h5CkqgopLJE VaFUQaXWQKPTw1RYhBJzGV48ehxHm0+hvLZ+14dFkuTK9Ob+CiNXL6PvV/+Or4e/wsy9aXgX5hD0 +xGOhFf9wenbI30HS05g0ur0MBUWo8RSju+dPIWXT76Ob4b+D3/+9GPcv3sHcw9nsLy0CP/yEgIB P0LBIMLhECLh+GcpEk0cjLZoOLMyJKRSKaQSKaQyWaJSkkOpSgSFVget3oDCohJUVO/Fy6/a0XDk GAyFpXnr33bZdGgAwMLsDH7zq1/g8icf4e7UbSzMz8LvW06cJM0sOQGGyNMo21FYKpFCJpdDqVRB qzegqNSMfbYGvPl37+HQSy9DmnhsYCQcxG3PCNzXvsTgV1dxd3IiHiCLXvh9ywj4fQgGAxkhkqxE Vn62gI19vtLv67JWSCgUytTQQ6VSQ63RQmcwwlRUjKraOjQefB7PHDoCc5UVEsn2PYcs33ISGgAQ Cwfx2Uf/g99++B+YunUT87MP4VteRDAQSP2hI+FwvORM/pEZHrtfYmdbuYPJ5QrIFfEJTHqDEUWl ZjzXdAxvvf0PKH7CrfZn701h7OvruPXNddwYHsLMg3tYXJjH0qIXvuUl+H2JoUwiRKKRCCKRCGLR KGKIiX2+EgGXDDmJVBo/N5HYBqVKFQ8JtQZanR5avQFGUyFKzWV4trERFdZnUGdrhM5UlIt3c1fI WWgkTY6N4L8/fB8To27Mzc5gybsAv285ccTwIxQMxAMkyvMeTwPJihOACqUKao0Wao02ftXAVIBS SwWOv3ICR0+ehmID06O9D+9hzPMNpm9+jRtD1zB5axze+bn4UCYRINkOTuuZdJgMO5lcDrlCAblc AYVKBbVaA7VGC43eAJOpEHutddjfcAjmKius++qh1u+8O6RvlZyHRlJgcRZjnlFM3xrH18NXMT42 ioW5h1ha9CLg88WPEInSknapxBE6eRJQq9NDbzShrKoGzzYegqWqDrV1VhiLLUAOy3W/dxYTN8cw dXMEN79xw+MZxbJ3IR4ifh+Cfj8ikTAikcjjP18SCWSJSVfJoNDq9NCbCrC/3oZqaz3Ka+tRVVML hVqXs/7vdnkLjZUioQCmxkdxa2wU46M3MH5zDKFg5oNlYrHYqnuGbtfPVtpJfdsp2w/Ej9SWsgpY 99tQWbsf1dZ9UOm29igci0aw+PAupianMHP/LpZm72Jm5gG8CwtYWFpGMBC/oheNhCFJzZ9QwaTX wWgyoai4BNoiC8x7zDCXlUFXaN7S/u82WxYaRPR0eHpP8RJRXjA0iEgIQ4OIhDA0iEgIQ4OIhDA0 iEgIQ4OIhDA0iEgIQ4OIhDA0iEgIQ4OIhDA0iEgIQ4OIhDA0iEgIQ4OIhDA0iEgIQ4OIhDA0iEgI Q4OIhDA0iEgIQ4OIhDA0iEgIQ4OIhDA0iEgIQ4OIhDA0iEgIQ4OIhDA0iEgIQ4OIhDA0iEgIQ4OI hDA0iEgIQ4OIhDA0iEgIQ4OIhDA0iEgIQ4OIhDA0iEgIQ4OIhDA0iEgIQ4OIhDA0iEgIQ4OIhDA0 iEgIQ4OIhDA0iEgIQ4OIhDA0iEgIQ4OIhDA0iEgIQ4OIhDA0iEgIQ4OIhDA0iEgIQ4OIhDA0iEgI Q4OIhDA0iEgIQ4OIhDA0iEgIQ4OIhDA0iEgIQ4OIhDA0iEgIQyNv+nDBIkFTa1/WVz1dB9BkOYBu 90baXbFcvxNNFknqn7NraKOdFufuhHND20G7FUMjrxphHenMskP1oTtXO3a/E02tw2gbiOHKdAxX pgdh7z+wdcFhu4je6UG02bZmdbT9GBp5Zq0HXP0rduD+PrhaHLBvuvUhdHf1wd6TvtM2oq27A+jq hGvT7ROtxtDIM2v7RVgzduAhdHcNo63dkfmL7k4404YYF/qzv+bsGk77+QdwuR2wt6xYqe0c7LZh eNxAcjhzofUAmizOeD8y2uvMGO64Wh/14dHQKjEk6nJmGQI9Gi7Fh1xZls/YtkQfsvVrzffDCReG 0N2ctt5+J5qaO+FZs33KF4ZG3jlgb+mDKxkC7g/gggP2jHK+DxeaO2DtSQwxBjrgaU3uyJmvXUJf 5k5ha4Q163qH4BlJ+7/tP3Fluhf2J7RnT/Zhuhf2/vSh1RC63Y74az0OeLJUMtb2wUfLwoFLPY5E //tgTw6fepA2NEvv18r3Y+UyQNtAbyKA+3ChFbg0cBHWx7ZP+cDQ2AL29g54uuJHRVdXR7z6SP8F 9zA8SKsYbBfR1pLY6d3D8Ng60JZ4zdp+MXMHcw/Fj7arNMJa/+j/9pbGR+t6XHupk6orj9iNj6qj FgfsSFYyq7lanUBPIgjcw/AkqoRk9eFxD6faTPVr1fuRbRkHLvUAFyzrbZ/yQb7dHfhWsJ2DHefh 6gdcIx24tHI4sZl2bR1w9SNziOL+AC53g/jJSXcnnK3ApekY7BhCd/N58T71O3EBvbiSsY0OXMpS TTxetmVy8bu0Waw0tkQj2tob0N3aAbScWz2csDXAivQhTCe6+xOVh60BVncHuhOvZQ4L4kd/V2t6 VRAffmBlBZG+rrXaGxmCJznccX8A10YuB7ciMSzJ3LYnDxnSLiWvuUyi/eleILnN626fcoWVxlZp uYg2G2Btz1KOw4FLAx1wNkvQBABoRNvAYGKnj58baGqNv2Zt74A9/Sjd0osrPfEhRZK9J4beNauZ x7TXchFtXQfgtHQAtpXnXZ4sGV6uZF9sHegduJjYtgNo6krv8+NacmRZpgNtIx3wtA/CjkZY2zvh bO58TPuOtRqnTfp/ll5DOOfC3YEAAAAASUVORK5CYII= "
- id="image4166-8-4"
- x="0"
- y="793.00507"
- style="fill:#006868;fill-opacity:1" />
- </g>
- <g
- style="display:inline;fill:#006868;fill-opacity:1"
- inkscape:label="Trace"
- id="layer2-9-1-52">
- <g
- id="g4216-4-8"
- style="fill:#006868;fill-opacity:1">
- <path
- style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#006868;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:45.99133301;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
- d="M 32.360048,65.606228 A 18.742595,18.742595 0 0 0 13.619017,84.347257 v 52.986143 52.98616 a 18.742595,18.742595 0 0 0 18.741031,18.74102 h 66.931502 52.98614 a 18.742595,18.742595 0 0 0 14.56367,-6.9571 c 9.39466,4.42246 19.71589,6.94877 30.41586,6.9571 a 18.742595,18.742595 0 0 0 0.0133,0 c 39.39268,0.001 71.72926,-32.3342 71.72947,-71.72718 -2.1e-4,-39.392981 -32.33679,-71.728471 -71.72977,-71.727172 a 18.742595,18.742595 0 0 0 -0.0106,0 c -18.56058,0.0092 -35.9395,7.273903 -48.98023,19.491814 C 135.2392,72.880337 117.85926,65.616053 99.29911,65.606228 a 18.742595,18.742595 0 0 0 -0.008,0 z M 32.20937,84.347257 H 99.14088 A 52.987154,52.987154 0 0 1 148.1289,117.2103 52.987154,52.987154 0 0 1 197.12213,84.347257 52.987154,52.987154 0 0 1 250.11087,137.3334 52.987154,52.987154 0 0 1 197.12213,190.31956 52.987154,52.987154 0 0 1 152.12702,165.2527 v 25.06686 H 99.14088 32.20937 V 137.3334 Z"
- id="path4176-3-9"
- inkscape:connector-curvature="0" />
- </g>
- </g>
- </g>
- </g>
-</svg>
diff --git a/src/stylesheets/Paper/Dark/backup.svg b/src/stylesheets/Paper/Dark/backup.svg
deleted file mode 100644
index f0010ea9..00000000
--- a/src/stylesheets/Paper/Dark/backup.svg
+++ /dev/null
@@ -1,128 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
- xmlns:dc="http://purl.org/dc/elements/1.1/"
- xmlns:cc="http://creativecommons.org/ns#"
- xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
- xmlns:svg="http://www.w3.org/2000/svg"
- xmlns="http://www.w3.org/2000/svg"
- xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
- xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
- width="20"
- height="20"
- viewBox="0 0 20 20"
- version="1.1"
- id="svg8"
- inkscape:export-filename="D:\Bethesda Game Modding\Mod Organizer 2\stylesheets\Paper\Dark\dots.png"
- inkscape:export-xdpi="307.20001"
- inkscape:export-ydpi="307.20001"
- inkscape:version="0.92.3 (2405546, 2018-03-11)"
- sodipodi:docname="backup.svg">
- <defs
- id="defs2" />
- <sodipodi:namedview
- id="base"
- pagecolor="#505050"
- bordercolor="#666666"
- borderopacity="1.0"
- inkscape:pageopacity="0"
- inkscape:pageshadow="2"
- inkscape:zoom="172.55859"
- inkscape:cx="9.017725"
- inkscape:cy="10"
- inkscape:document-units="px"
- inkscape:current-layer="layer25"
- showgrid="false"
- units="px"
- borderlayer="true"
- inkscape:showpageshadow="false"
- inkscape:snap-page="true"
- inkscape:snap-bbox="true"
- inkscape:bbox-nodes="true"
- inkscape:snap-bbox-edge-midpoints="true"
- inkscape:snap-bbox-midpoints="true"
- showguides="true"
- inkscape:guide-bbox="true"
- inkscape:window-width="1920"
- inkscape:window-height="1027"
- inkscape:window-x="-8"
- inkscape:window-y="-8"
- inkscape:window-maximized="1"
- inkscape:snap-center="true"
- inkscape:snap-object-midpoints="true"
- inkscape:measure-start="18.8975,12.498"
- inkscape:measure-end="20,12.498"
- inkscape:snap-intersection-paths="true"
- inkscape:object-paths="true"
- inkscape:snap-smooth-nodes="true"
- inkscape:snap-midpoints="true"
- inkscape:snap-global="true">
- <sodipodi:guide
- position="10,10"
- orientation="1,0"
- id="guide4039"
- inkscape:locked="false" />
- <sodipodi:guide
- position="10,10"
- orientation="0,1"
- id="guide3935"
- inkscape:locked="false" />
- </sodipodi:namedview>
- <metadata
- id="metadata5">
- <rdf:RDF>
- <cc:Work
- rdf:about="">
- <dc:format>image/svg+xml</dc:format>
- <dc:type
- rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
- <dc:title />
- </cc:Work>
- </rdf:RDF>
- </metadata>
- <g
- inkscape:groupmode="layer"
- id="layer25"
- inkscape:label="Content">
- <g
- id="g3865"
- transform="matrix(1,0,0,-1,0,20)"
- style="fill:#ffffff;fill-opacity:1">
- <path
- id="rect3928"
- d="m 0,14 v 3 3 h 20 v -3 -3 h -3 v 3 H 3 v -3 z"
- style="opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.40999997;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:normal"
- inkscape:connector-curvature="0" />
- <g
- id="g3859"
- style="fill:#ffffff;fill-opacity:1">
- <rect
- y="-12.83"
- x="7.85585e-016"
- height="5.6599998"
- width="13.981334"
- id="rect3930"
- style="opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.40999997;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:normal"
- transform="rotate(90)" />
- <path
- sodipodi:type="star"
- style="opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.83595276;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:normal"
- id="path3932"
- sodipodi:sides="3"
- sodipodi:cx="8.9146461"
- sodipodi:cy="9.684866"
- sodipodi:r1="4.0292788"
- sodipodi:r2="2.0146394"
- sodipodi:arg1="-1.5707963"
- sodipodi:arg2="-0.52359878"
- inkscape:flatsided="true"
- inkscape:rounded="0"
- inkscape:randomized="0"
- d="m 8.9146463,5.6555872 3.4894577,6.0439178 -6.9789157,0 z"
- inkscape:transform-center-y="-1.3333344"
- transform="matrix(-2.1493311,0,0,-1.3236446,29.160526,24.485987)" />
- </g>
- </g>
- </g>
-</svg>
diff --git a/src/stylesheets/Paper/Dark/check-alt.svg b/src/stylesheets/Paper/Dark/check-alt.svg
deleted file mode 100644
index 90b924f0..00000000
--- a/src/stylesheets/Paper/Dark/check-alt.svg
+++ /dev/null
@@ -1,94 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
- xmlns:dc="http://purl.org/dc/elements/1.1/"
- xmlns:cc="http://creativecommons.org/ns#"
- xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
- xmlns:svg="http://www.w3.org/2000/svg"
- xmlns="http://www.w3.org/2000/svg"
- xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
- xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
- width="20"
- height="20"
- viewBox="0 0 20 20"
- version="1.1"
- id="svg8"
- inkscape:export-filename="D:\Bethesda Game Modding\Mod Organizer 2\stylesheets\Paper\Dark\dots.png"
- inkscape:export-xdpi="307.20001"
- inkscape:export-ydpi="307.20001"
- inkscape:version="0.92.1 r15371"
- sodipodi:docname="check-alt.svg">
- <defs
- id="defs2" />
- <sodipodi:namedview
- id="base"
- pagecolor="#505050"
- bordercolor="#666666"
- borderopacity="1.0"
- inkscape:pageopacity="0"
- inkscape:pageshadow="2"
- inkscape:zoom="18.564955"
- inkscape:cx="1.4921607"
- inkscape:cy="9.294465"
- inkscape:document-units="px"
- inkscape:current-layer="layer25"
- showgrid="false"
- units="px"
- borderlayer="true"
- inkscape:showpageshadow="false"
- inkscape:snap-page="true"
- inkscape:snap-bbox="true"
- inkscape:bbox-nodes="true"
- inkscape:snap-bbox-edge-midpoints="true"
- inkscape:snap-bbox-midpoints="true"
- showguides="true"
- inkscape:guide-bbox="true"
- inkscape:window-width="1920"
- inkscape:window-height="1027"
- inkscape:window-x="-8"
- inkscape:window-y="-8"
- inkscape:window-maximized="1"
- inkscape:snap-center="true"
- inkscape:snap-object-midpoints="true"
- inkscape:measure-start="18.8975,12.498"
- inkscape:measure-end="20,12.498"
- inkscape:snap-intersection-paths="true"
- inkscape:object-paths="true"
- inkscape:snap-smooth-nodes="true"
- inkscape:snap-midpoints="true"
- inkscape:snap-global="true">
- <sodipodi:guide
- position="10,10"
- orientation="1,0"
- id="guide4039"
- inkscape:locked="false" />
- <sodipodi:guide
- position="10,10"
- orientation="0,1"
- id="guide3935"
- inkscape:locked="false" />
- </sodipodi:namedview>
- <metadata
- id="metadata5">
- <rdf:RDF>
- <cc:Work
- rdf:about="">
- <dc:format>image/svg+xml</dc:format>
- <dc:type
- rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
- <dc:title></dc:title>
- </cc:Work>
- </rdf:RDF>
- </metadata>
- <g
- inkscape:groupmode="layer"
- id="layer25"
- inkscape:label="Content">
- <path
- style="display:inline;opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:169.70561218;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:1.93939996;stroke-opacity:1;paint-order:markers stroke fill"
- d="m 16,2.0000002 -4,4 -4.0000001,3.9999996 -4,-3.9999996 L -9.4999999e-8,9.9999998 3.9999999,14 l 4,4 L 12,14 16,9.9999998 20,6.0000002 Z"
- id="rect3900-47"
- inkscape:connector-curvature="0" />
- </g>
-</svg>
diff --git a/src/stylesheets/Paper/Dark/check.svg b/src/stylesheets/Paper/Dark/check.svg
deleted file mode 100644
index 2d113294..00000000
--- a/src/stylesheets/Paper/Dark/check.svg
+++ /dev/null
@@ -1,94 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
- xmlns:dc="http://purl.org/dc/elements/1.1/"
- xmlns:cc="http://creativecommons.org/ns#"
- xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
- xmlns:svg="http://www.w3.org/2000/svg"
- xmlns="http://www.w3.org/2000/svg"
- xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
- xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
- width="20"
- height="20"
- viewBox="0 0 20 20"
- version="1.1"
- id="svg8"
- inkscape:export-filename="D:\Bethesda Game Modding\Mod Organizer 2\stylesheets\Paper\Dark\dots.png"
- inkscape:export-xdpi="307.20001"
- inkscape:export-ydpi="307.20001"
- inkscape:version="0.92.3 (2405546, 2018-03-11)"
- sodipodi:docname="check.svg">
- <defs
- id="defs2" />
- <sodipodi:namedview
- id="base"
- pagecolor="#505050"
- bordercolor="#666666"
- borderopacity="1.0"
- inkscape:pageopacity="0"
- inkscape:pageshadow="2"
- inkscape:zoom="11.625"
- inkscape:cx="-3.537598"
- inkscape:cy="-2.9645926"
- inkscape:document-units="px"
- inkscape:current-layer="layer25"
- showgrid="false"
- units="px"
- borderlayer="true"
- inkscape:showpageshadow="false"
- inkscape:snap-page="true"
- inkscape:snap-bbox="true"
- inkscape:bbox-nodes="true"
- inkscape:snap-bbox-edge-midpoints="true"
- inkscape:snap-bbox-midpoints="true"
- showguides="false"
- inkscape:guide-bbox="true"
- inkscape:window-width="1920"
- inkscape:window-height="1027"
- inkscape:window-x="-8"
- inkscape:window-y="-8"
- inkscape:window-maximized="1"
- inkscape:snap-center="true"
- inkscape:snap-object-midpoints="true"
- inkscape:measure-start="18.8975,12.498"
- inkscape:measure-end="20,12.498"
- inkscape:snap-intersection-paths="true"
- inkscape:object-paths="true"
- inkscape:snap-smooth-nodes="true"
- inkscape:snap-midpoints="true"
- inkscape:snap-global="true">
- <sodipodi:guide
- position="10,10"
- orientation="1,0"
- id="guide4039"
- inkscape:locked="false" />
- <sodipodi:guide
- position="10,10"
- orientation="0,1"
- id="guide3935"
- inkscape:locked="false" />
- </sodipodi:namedview>
- <metadata
- id="metadata5">
- <rdf:RDF>
- <cc:Work
- rdf:about="">
- <dc:format>image/svg+xml</dc:format>
- <dc:type
- rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
- <dc:title />
- </cc:Work>
- </rdf:RDF>
- </metadata>
- <g
- inkscape:groupmode="layer"
- id="layer25"
- inkscape:label="Content">
- <path
- style="display:inline;opacity:1;fill:#007e7e;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:169.70561218;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:1.93939996;stroke-opacity:1;paint-order:markers stroke fill"
- d="m 16,2.0000002 -4,4 -4.0000001,3.9999996 -4,-3.9999996 L -9.4999999e-8,9.9999998 3.9999999,14 l 4,4 L 12,14 16,9.9999998 20,6.0000002 Z"
- id="rect3900-47"
- inkscape:connector-curvature="0" />
- </g>
-</svg>
diff --git a/src/stylesheets/Paper/Dark/cross.svg b/src/stylesheets/Paper/Dark/cross.svg
deleted file mode 100644
index ac286c08..00000000
--- a/src/stylesheets/Paper/Dark/cross.svg
+++ /dev/null
@@ -1,94 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
- xmlns:dc="http://purl.org/dc/elements/1.1/"
- xmlns:cc="http://creativecommons.org/ns#"
- xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
- xmlns:svg="http://www.w3.org/2000/svg"
- xmlns="http://www.w3.org/2000/svg"
- xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
- xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
- width="20"
- height="20"
- viewBox="0 0 20 20"
- version="1.1"
- id="svg8"
- inkscape:export-filename="D:\Bethesda Game Modding\Mod Organizer 2\stylesheets\Paper\Dark\dots.png"
- inkscape:export-xdpi="307.20001"
- inkscape:export-ydpi="307.20001"
- inkscape:version="0.92.1 r15371"
- sodipodi:docname="cross.svg">
- <defs
- id="defs2" />
- <sodipodi:namedview
- id="base"
- pagecolor="#505050"
- bordercolor="#666666"
- borderopacity="1.0"
- inkscape:pageopacity="0"
- inkscape:pageshadow="2"
- inkscape:zoom="17.223347"
- inkscape:cx="-5.6646738"
- inkscape:cy="4.7813622"
- inkscape:document-units="px"
- inkscape:current-layer="layer25"
- showgrid="false"
- units="px"
- borderlayer="true"
- inkscape:showpageshadow="false"
- inkscape:snap-page="true"
- inkscape:snap-bbox="true"
- inkscape:bbox-nodes="true"
- inkscape:snap-bbox-edge-midpoints="true"
- inkscape:snap-bbox-midpoints="true"
- showguides="true"
- inkscape:guide-bbox="true"
- inkscape:window-width="1920"
- inkscape:window-height="1027"
- inkscape:window-x="-8"
- inkscape:window-y="-8"
- inkscape:window-maximized="1"
- inkscape:snap-center="true"
- inkscape:snap-object-midpoints="true"
- inkscape:measure-start="18.8975,12.498"
- inkscape:measure-end="20,12.498"
- inkscape:snap-intersection-paths="true"
- inkscape:object-paths="true"
- inkscape:snap-smooth-nodes="true"
- inkscape:snap-midpoints="true"
- inkscape:snap-global="true">
- <sodipodi:guide
- position="10,10"
- orientation="1,0"
- id="guide4039"
- inkscape:locked="false" />
- <sodipodi:guide
- position="10,10"
- orientation="0,1"
- id="guide3935"
- inkscape:locked="false" />
- </sodipodi:namedview>
- <metadata
- id="metadata5">
- <rdf:RDF>
- <cc:Work
- rdf:about="">
- <dc:format>image/svg+xml</dc:format>
- <dc:type
- rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
- <dc:title></dc:title>
- </cc:Work>
- </rdf:RDF>
- </metadata>
- <g
- inkscape:groupmode="layer"
- id="layer25"
- inkscape:label="Content">
- <path
- style="display:inline;opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:169.70561218;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:1.93939996;stroke-opacity:1;paint-order:markers stroke fill"
- d="m 4,1.5e-7 -4,3.99999995 2,2 L 6,10 l -4,4 -2,2 4,4 2,-2 4,-4 4,4 2,2 4,-4 -2,-2 -4,-4 4,-3.9999999 2,-2 L 16,1.5e-7 14,2.0000002 10,6.0000001 6,2.0000002 Z"
- id="path3944"
- inkscape:connector-curvature="0" />
- </g>
-</svg>
diff --git a/src/stylesheets/Paper/Dark/dots.svg b/src/stylesheets/Paper/Dark/dots.svg
deleted file mode 100644
index a746ec83..00000000
--- a/src/stylesheets/Paper/Dark/dots.svg
+++ /dev/null
@@ -1,111 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
- xmlns:dc="http://purl.org/dc/elements/1.1/"
- xmlns:cc="http://creativecommons.org/ns#"
- xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
- xmlns:svg="http://www.w3.org/2000/svg"
- xmlns="http://www.w3.org/2000/svg"
- xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
- xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
- width="20"
- height="20"
- viewBox="0 0 20 20"
- version="1.1"
- id="svg8"
- inkscape:export-filename="D:\Bethesda Game Modding\Mod Organizer 2\stylesheets\Paper\Dark\dots.png"
- inkscape:export-xdpi="307.20001"
- inkscape:export-ydpi="307.20001"
- inkscape:version="0.92.3 (2405546, 2018-03-11)"
- sodipodi:docname="dots.svg">
- <defs
- id="defs2" />
- <sodipodi:namedview
- id="base"
- pagecolor="#505050"
- bordercolor="#666666"
- borderopacity="1.0"
- inkscape:pageopacity="0"
- inkscape:pageshadow="2"
- inkscape:zoom="90.389123"
- inkscape:cx="0.38783507"
- inkscape:cy="6.4385372"
- inkscape:document-units="px"
- inkscape:current-layer="layer25"
- showgrid="false"
- units="px"
- borderlayer="true"
- inkscape:showpageshadow="false"
- inkscape:snap-page="true"
- inkscape:snap-bbox="true"
- inkscape:bbox-nodes="true"
- inkscape:snap-bbox-edge-midpoints="true"
- inkscape:snap-bbox-midpoints="true"
- showguides="true"
- inkscape:guide-bbox="true"
- inkscape:window-width="1920"
- inkscape:window-height="1027"
- inkscape:window-x="-8"
- inkscape:window-y="-8"
- inkscape:window-maximized="1"
- inkscape:snap-center="true"
- inkscape:snap-object-midpoints="true"
- inkscape:measure-start="18.8975,12.498"
- inkscape:measure-end="20,12.498"
- inkscape:snap-intersection-paths="true"
- inkscape:object-paths="true"
- inkscape:snap-smooth-nodes="true"
- inkscape:snap-midpoints="true"
- inkscape:snap-global="true">
- <sodipodi:guide
- position="10,10"
- orientation="1,0"
- id="guide4039"
- inkscape:locked="false" />
- <sodipodi:guide
- position="10,10"
- orientation="0,1"
- id="guide3935"
- inkscape:locked="false" />
- </sodipodi:namedview>
- <metadata
- id="metadata5">
- <rdf:RDF>
- <cc:Work
- rdf:about="">
- <dc:format>image/svg+xml</dc:format>
- <dc:type
- rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
- <dc:title />
- </cc:Work>
- </rdf:RDF>
- </metadata>
- <g
- inkscape:groupmode="layer"
- id="layer25"
- inkscape:label="Content">
- <g
- id="g3853"
- style="fill:#ffffff;fill-opacity:1">
- <circle
- r="2.5"
- cy="10"
- cx="10"
- id="path4063"
- style="opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:3.99999952;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:stroke fill markers" />
- <circle
- style="opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:3.99999952;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:stroke fill markers"
- id="circle3937"
- cx="17.5"
- cy="10"
- r="2.5" />
- <circle
- r="2.5"
- cy="10"
- cx="2.5"
- id="circle3939"
- style="opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:3.99999952;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:stroke fill markers" />
- </g>
- </g>
-</svg>
diff --git a/src/stylesheets/Paper/Dark/folder.svg b/src/stylesheets/Paper/Dark/folder.svg
deleted file mode 100644
index 84cebc66..00000000
--- a/src/stylesheets/Paper/Dark/folder.svg
+++ /dev/null
@@ -1,94 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
- xmlns:dc="http://purl.org/dc/elements/1.1/"
- xmlns:cc="http://creativecommons.org/ns#"
- xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
- xmlns:svg="http://www.w3.org/2000/svg"
- xmlns="http://www.w3.org/2000/svg"
- xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
- xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
- width="20"
- height="20"
- viewBox="0 0 20 20"
- version="1.1"
- id="svg8"
- inkscape:export-filename="D:\Bethesda Game Modding\Mod Organizer 2\stylesheets\Paper\Dark\dots.png"
- inkscape:export-xdpi="307.20001"
- inkscape:export-ydpi="307.20001"
- inkscape:version="0.92.1 r15371"
- sodipodi:docname="folder.svg">
- <defs
- id="defs2" />
- <sodipodi:namedview
- id="base"
- pagecolor="#505050"
- bordercolor="#666666"
- borderopacity="1.0"
- inkscape:pageopacity="0"
- inkscape:pageshadow="2"
- inkscape:zoom="12.178745"
- inkscape:cx="16.395211"
- inkscape:cy="11.770603"
- inkscape:document-units="px"
- inkscape:current-layer="layer25"
- showgrid="false"
- units="px"
- borderlayer="true"
- inkscape:showpageshadow="false"
- inkscape:snap-page="true"
- inkscape:snap-bbox="true"
- inkscape:bbox-nodes="true"
- inkscape:snap-bbox-edge-midpoints="true"
- inkscape:snap-bbox-midpoints="true"
- showguides="true"
- inkscape:guide-bbox="true"
- inkscape:window-width="1920"
- inkscape:window-height="1027"
- inkscape:window-x="-8"
- inkscape:window-y="-8"
- inkscape:window-maximized="1"
- inkscape:snap-center="true"
- inkscape:snap-object-midpoints="true"
- inkscape:measure-start="18.8975,12.498"
- inkscape:measure-end="20,12.498"
- inkscape:snap-intersection-paths="true"
- inkscape:object-paths="true"
- inkscape:snap-smooth-nodes="true"
- inkscape:snap-midpoints="true"
- inkscape:snap-global="true">
- <sodipodi:guide
- position="10,10"
- orientation="1,0"
- id="guide4039"
- inkscape:locked="false" />
- <sodipodi:guide
- position="10,10"
- orientation="0,1"
- id="guide3935"
- inkscape:locked="false" />
- </sodipodi:namedview>
- <metadata
- id="metadata5">
- <rdf:RDF>
- <cc:Work
- rdf:about="">
- <dc:format>image/svg+xml</dc:format>
- <dc:type
- rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
- <dc:title></dc:title>
- </cc:Work>
- </rdf:RDF>
- </metadata>
- <g
- inkscape:groupmode="layer"
- id="layer25"
- inkscape:label="Content">
- <path
- inkscape:connector-curvature="0"
- id="path3850"
- d="M 12,2.0000002 9.9999996,4.0000001 H 8.4444481e-8 V 18 H 20 V 4.0000001 L 17.999999,2.0000002 H 15 Z"
- style="display:inline;opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:5.99999952;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:stroke markers fill" />
- </g>
-</svg>
diff --git a/src/stylesheets/Paper/Dark/heart.svg b/src/stylesheets/Paper/Dark/heart.svg
deleted file mode 100644
index 5a4cc1f1..00000000
--- a/src/stylesheets/Paper/Dark/heart.svg
+++ /dev/null
@@ -1,111 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
- xmlns:dc="http://purl.org/dc/elements/1.1/"
- xmlns:cc="http://creativecommons.org/ns#"
- xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
- xmlns:svg="http://www.w3.org/2000/svg"
- xmlns="http://www.w3.org/2000/svg"
- xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
- xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
- width="20"
- height="20"
- viewBox="0 0 20 20"
- version="1.1"
- id="svg8"
- inkscape:export-filename="D:\Bethesda Game Modding\Mod Organizer 2\stylesheets\Paper\Dark\dots.png"
- inkscape:export-xdpi="307.20001"
- inkscape:export-ydpi="307.20001"
- inkscape:version="0.92.1 r15371"
- sodipodi:docname="heart.svg">
- <defs
- id="defs2" />
- <sodipodi:namedview
- id="base"
- pagecolor="#505050"
- bordercolor="#666666"
- borderopacity="1.0"
- inkscape:pageopacity="0"
- inkscape:pageshadow="2"
- inkscape:zoom="17.223347"
- inkscape:cx="-3.039376"
- inkscape:cy="10.121962"
- inkscape:document-units="px"
- inkscape:current-layer="layer25"
- showgrid="false"
- units="px"
- borderlayer="true"
- inkscape:showpageshadow="false"
- inkscape:snap-page="true"
- inkscape:snap-bbox="true"
- inkscape:bbox-nodes="true"
- inkscape:snap-bbox-edge-midpoints="true"
- inkscape:snap-bbox-midpoints="true"
- showguides="true"
- inkscape:guide-bbox="true"
- inkscape:window-width="1920"
- inkscape:window-height="1027"
- inkscape:window-x="-8"
- inkscape:window-y="-8"
- inkscape:window-maximized="1"
- inkscape:snap-center="true"
- inkscape:snap-object-midpoints="true"
- inkscape:measure-start="18.8975,12.498"
- inkscape:measure-end="20,12.498"
- inkscape:snap-intersection-paths="true"
- inkscape:object-paths="true"
- inkscape:snap-smooth-nodes="true"
- inkscape:snap-midpoints="true"
- inkscape:snap-global="true">
- <sodipodi:guide
- position="10,10"
- orientation="1,0"
- id="guide4039"
- inkscape:locked="false" />
- <sodipodi:guide
- position="10,10"
- orientation="0,1"
- id="guide3935"
- inkscape:locked="false" />
- </sodipodi:namedview>
- <metadata
- id="metadata5">
- <rdf:RDF>
- <cc:Work
- rdf:about="">
- <dc:format>image/svg+xml</dc:format>
- <dc:type
- rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
- <dc:title></dc:title>
- </cc:Work>
- </rdf:RDF>
- </metadata>
- <g
- inkscape:groupmode="layer"
- id="layer25"
- inkscape:label="Content">
- <g
- style="display:inline"
- id="g4198"
- transform="translate(0,1)">
- <circle
- r="5.5"
- cy="5.5"
- cx="5.5"
- id="path4174"
- style="opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:5.99999952;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:normal" />
- <circle
- r="5.5"
- style="opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:6;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:normal"
- id="circle4176"
- cx="14.5"
- cy="5.5" />
- <path
- id="path4178"
- d="M 0,5.5 C 0,10.950465 5.8158604,15.070234 10,18 14.18414,15.070234 20,10.950465 20,5.5 H 10 Z"
- style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.99999994px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
- inkscape:connector-curvature="0" />
- </g>
- </g>
-</svg>
diff --git a/src/stylesheets/Paper/Dark/highlight.svg b/src/stylesheets/Paper/Dark/highlight.svg
deleted file mode 100644
index 9dee51d8..00000000
--- a/src/stylesheets/Paper/Dark/highlight.svg
+++ /dev/null
@@ -1,205 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
- xmlns:dc="http://purl.org/dc/elements/1.1/"
- xmlns:cc="http://creativecommons.org/ns#"
- xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
- xmlns:svg="http://www.w3.org/2000/svg"
- xmlns="http://www.w3.org/2000/svg"
- xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
- xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
- width="128"
- height="64"
- viewBox="0 0 128 64"
- version="1.1"
- id="svg8"
- inkscape:export-filename="D:\Bethesda Game Modding\Mod Organizer 2\stylesheets\Paper\Dark\dots.png"
- inkscape:export-xdpi="307.20001"
- inkscape:export-ydpi="307.20001"
- inkscape:version="0.92.3 (2405546, 2018-03-11)"
- sodipodi:docname="highlight.svg">
- <defs
- id="defs2">
- <meshgradient
- inkscape:collect="always"
- id="meshgradient3955"
- gradientUnits="userSpaceOnUse"
- x="0"
- y="-44">
- <meshrow
- id="meshrow18393">
- <meshpatch
- id="meshpatch18395">
- <stop
- path="c 4.96082,0 9.92164,0 14.8825,0"
- style="stop-color:#008484;stop-opacity:1"
- id="stop18397" />
- <stop
- path="c 0,10.6667 0,21.3333 0,32"
- style="stop-color:#008484;stop-opacity:1"
- id="stop18399" />
- <stop
- path="c -4.96082,1.77636e-015 -9.92165,1.77636e-015 -14.8825,1.77636e-015"
- style="stop-color:#006868;stop-opacity:1"
- id="stop18401" />
- <stop
- path="c 0,-10.6667 0,-21.3333 0,-32"
- style="stop-color:#008484;stop-opacity:1"
- id="stop18403" />
- </meshpatch>
- <meshpatch
- id="meshpatch18405">
- <stop
- path="c 16.3725,0 32.7451,0 49.1175,0"
- id="stop18407" />
- <stop
- path="c 0,10.6667 0,21.3333 0,32"
- style="stop-color:#008484;stop-opacity:1"
- id="stop18409" />
- <stop
- path="c -16.3725,0 -32.745,0 -49.1175,0"
- style="stop-color:#006868;stop-opacity:1"
- id="stop18411" />
- </meshpatch>
- <meshpatch
- id="meshpatch18413">
- <stop
- path="c 17.6536,0 35.3073,0 52.9609,0"
- id="stop18415" />
- <stop
- path="c 0,10.6667 0,21.3333 0,32"
- style="stop-color:#008484;stop-opacity:1"
- id="stop18417" />
- <stop
- path="c -17.6536,0 -35.3073,0 -52.9609,0"
- style="stop-color:#006868;stop-opacity:1"
- id="stop18419" />
- </meshpatch>
- <meshpatch
- id="meshpatch18421">
- <stop
- path="c 3.67971,0 7.35941,0 11.0391,0"
- id="stop18423" />
- <stop
- path="c 0,10.6667 0,21.3333 0,32"
- style="stop-color:#008484;stop-opacity:1"
- id="stop18425" />
- <stop
- path="c -3.6797,0 -7.35941,0 -11.0391,0"
- style="stop-color:#008484;stop-opacity:1"
- id="stop18427" />
- </meshpatch>
- </meshrow>
- <meshrow
- id="meshrow18429">
- <meshpatch
- id="meshpatch18431">
- <stop
- path="c 0,10.6667 0,21.3333 0,32"
- id="stop18433" />
- <stop
- path="c -4.96082,-3.55271e-015 -9.92165,-3.55271e-015 -14.8825,-3.55271e-015"
- style="stop-color:#007d7d;stop-opacity:1"
- id="stop18435" />
- <stop
- path="c 0,-10.6667 0,-21.3333 0,-32"
- style="stop-color:#008484;stop-opacity:1"
- id="stop18437" />
- </meshpatch>
- <meshpatch
- id="meshpatch18439">
- <stop
- path="c 0,10.6667 0,21.3333 0,32"
- id="stop18441" />
- <stop
- path="c -16.3725,0 -32.745,0 -49.1175,0"
- style="stop-color:#008484;stop-opacity:1"
- id="stop18443" />
- </meshpatch>
- <meshpatch
- id="meshpatch18445">
- <stop
- path="c 7.38594e-006,10.6667 0.000207035,21.333 2.21578e-005,32"
- id="stop18447" />
- <stop
- path="c -17.6536,-4.55705e-005 -35.3071,-0.000147247 -52.9609,1.82555e-005"
- style="stop-color:#007f7f;stop-opacity:1"
- id="stop18449" />
- </meshpatch>
- <meshpatch
- id="meshpatch18451">
- <stop
- path="c 0,10.6667 0.000232338,21.3329 0,32"
- id="stop18453" />
- <stop
- path="c -3.6797,-1.69572e-006 -7.3594,-8.75677e-006 -11.0391,-1.82555e-005"
- style="stop-color:#008484;stop-opacity:1"
- id="stop18455" />
- </meshpatch>
- </meshrow>
- </meshgradient>
- </defs>
- <sodipodi:namedview
- id="base"
- pagecolor="#505050"
- bordercolor="#666666"
- borderopacity="1.0"
- inkscape:pageopacity="0"
- inkscape:pageshadow="2"
- inkscape:zoom="5.2410685"
- inkscape:cx="55.924572"
- inkscape:cy="37.651744"
- inkscape:document-units="px"
- inkscape:current-layer="layer25"
- showgrid="false"
- units="px"
- borderlayer="true"
- inkscape:showpageshadow="false"
- inkscape:snap-page="true"
- inkscape:snap-bbox="true"
- inkscape:bbox-nodes="true"
- inkscape:snap-bbox-edge-midpoints="true"
- inkscape:snap-bbox-midpoints="true"
- showguides="true"
- inkscape:guide-bbox="true"
- inkscape:window-width="1920"
- inkscape:window-height="1027"
- inkscape:window-x="-8"
- inkscape:window-y="-8"
- inkscape:window-maximized="1"
- inkscape:snap-center="true"
- inkscape:snap-object-midpoints="true"
- inkscape:measure-start="18.8975,12.498"
- inkscape:measure-end="20,12.498"
- inkscape:snap-intersection-paths="true"
- inkscape:object-paths="true"
- inkscape:snap-smooth-nodes="true"
- inkscape:snap-midpoints="true"
- inkscape:snap-global="true" />
- <metadata
- id="metadata5">
- <rdf:RDF>
- <cc:Work
- rdf:about="">
- <dc:format>image/svg+xml</dc:format>
- <dc:type
- rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
- <dc:title></dc:title>
- </cc:Work>
- </rdf:RDF>
- </metadata>
- <g
- inkscape:groupmode="layer"
- id="layer25"
- inkscape:label="Content"
- transform="translate(0,44)">
- <rect
- style="opacity:1;fill:url(#meshgradient3955);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:markers stroke fill"
- id="rect3848"
- width="128"
- height="64"
- x="0"
- y="-44" />
- </g>
-</svg>
diff --git a/src/stylesheets/Paper/Dark/refresh.svg b/src/stylesheets/Paper/Dark/refresh.svg
deleted file mode 100644
index a81507d5..00000000
--- a/src/stylesheets/Paper/Dark/refresh.svg
+++ /dev/null
@@ -1,103 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
- xmlns:dc="http://purl.org/dc/elements/1.1/"
- xmlns:cc="http://creativecommons.org/ns#"
- xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
- xmlns:svg="http://www.w3.org/2000/svg"
- xmlns="http://www.w3.org/2000/svg"
- xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
- xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
- width="20"
- height="20"
- viewBox="0 0 20 20"
- version="1.1"
- id="svg8"
- inkscape:export-filename="D:\Bethesda Game Modding\Mod Organizer 2\stylesheets\Paper\Dark\dots.png"
- inkscape:export-xdpi="307.20001"
- inkscape:export-ydpi="307.20001"
- inkscape:version="0.92.1 r15371"
- sodipodi:docname="refresh.svg">
- <defs
- id="defs2" />
- <sodipodi:namedview
- id="base"
- pagecolor="#505050"
- bordercolor="#666666"
- borderopacity="1.0"
- inkscape:pageopacity="0"
- inkscape:pageshadow="2"
- inkscape:zoom="18.564955"
- inkscape:cx="1.4921607"
- inkscape:cy="9.294465"
- inkscape:document-units="px"
- inkscape:current-layer="layer25"
- showgrid="false"
- units="px"
- borderlayer="true"
- inkscape:showpageshadow="false"
- inkscape:snap-page="true"
- inkscape:snap-bbox="true"
- inkscape:bbox-nodes="true"
- inkscape:snap-bbox-edge-midpoints="true"
- inkscape:snap-bbox-midpoints="true"
- showguides="true"
- inkscape:guide-bbox="true"
- inkscape:window-width="1920"
- inkscape:window-height="1027"
- inkscape:window-x="-8"
- inkscape:window-y="-8"
- inkscape:window-maximized="1"
- inkscape:snap-center="true"
- inkscape:snap-object-midpoints="true"
- inkscape:measure-start="18.8975,12.498"
- inkscape:measure-end="20,12.498"
- inkscape:snap-intersection-paths="true"
- inkscape:object-paths="true"
- inkscape:snap-smooth-nodes="true"
- inkscape:snap-midpoints="true"
- inkscape:snap-global="true">
- <sodipodi:guide
- position="10,10"
- orientation="1,0"
- id="guide4039"
- inkscape:locked="false" />
- <sodipodi:guide
- position="10,10"
- orientation="0,1"
- id="guide3935"
- inkscape:locked="false" />
- </sodipodi:namedview>
- <metadata
- id="metadata5">
- <rdf:RDF>
- <cc:Work
- rdf:about="">
- <dc:format>image/svg+xml</dc:format>
- <dc:type
- rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
- <dc:title></dc:title>
- </cc:Work>
- </rdf:RDF>
- </metadata>
- <g
- inkscape:groupmode="layer"
- id="layer25"
- inkscape:label="Content">
- <g
- style="display:inline"
- id="g3986">
- <path
- inkscape:connector-curvature="0"
- id="path4137"
- d="M 0,11.998047 V 20 L 2.9335938,17.066406 A 10,10 0 0 0 10,20 10,10 0 0 0 19.787109,11.998047 H 16.705078 A 7,7 0 0 1 10,17 7,7 0 0 1 5.0527344,14.947266 L 8,11.998047 Z"
- style="opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:6;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:normal" />
- <path
- inkscape:connector-curvature="0"
- id="rect4142"
- d="M 10,0 A 10,10 0 0 0 0.21289062,8.0019531 H 3.2949219 A 7,7 0 0 1 10,3 7,7 0 0 1 14.947266,5.0527344 L 12,8.0019531 h 8 V 0 L 17.066406,2.9335938 A 10,10 0 0 0 10,0 Z"
- style="opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:6;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:normal" />
- </g>
- </g>
-</svg>
diff --git a/src/stylesheets/Paper/Dark/restore-alt.svg b/src/stylesheets/Paper/Dark/restore-alt.svg
deleted file mode 100644
index 8f8c2a43..00000000
--- a/src/stylesheets/Paper/Dark/restore-alt.svg
+++ /dev/null
@@ -1,125 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
- xmlns:dc="http://purl.org/dc/elements/1.1/"
- xmlns:cc="http://creativecommons.org/ns#"
- xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
- xmlns:svg="http://www.w3.org/2000/svg"
- xmlns="http://www.w3.org/2000/svg"
- xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
- xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
- width="20"
- height="20"
- viewBox="0 0 20 20"
- version="1.1"
- id="svg8"
- inkscape:export-filename="D:\Bethesda Game Modding\Mod Organizer 2\stylesheets\Paper\Dark\dots.png"
- inkscape:export-xdpi="307.20001"
- inkscape:export-ydpi="307.20001"
- inkscape:version="0.92.1 r15371"
- sodipodi:docname="restore-alt.svg">
- <defs
- id="defs2" />
- <sodipodi:namedview
- id="base"
- pagecolor="#505050"
- bordercolor="#666666"
- borderopacity="1.0"
- inkscape:pageopacity="0"
- inkscape:pageshadow="2"
- inkscape:zoom="20.964273"
- inkscape:cx="9.5550394"
- inkscape:cy="5.9520353"
- inkscape:document-units="px"
- inkscape:current-layer="layer25"
- showgrid="false"
- units="px"
- borderlayer="true"
- inkscape:showpageshadow="false"
- inkscape:snap-page="true"
- inkscape:snap-bbox="true"
- inkscape:bbox-nodes="true"
- inkscape:snap-bbox-edge-midpoints="true"
- inkscape:snap-bbox-midpoints="true"
- showguides="true"
- inkscape:guide-bbox="true"
- inkscape:window-width="1920"
- inkscape:window-height="1027"
- inkscape:window-x="-8"
- inkscape:window-y="-8"
- inkscape:window-maximized="1"
- inkscape:snap-center="true"
- inkscape:snap-object-midpoints="true"
- inkscape:measure-start="18.8975,12.498"
- inkscape:measure-end="20,12.498"
- inkscape:snap-intersection-paths="true"
- inkscape:object-paths="true"
- inkscape:snap-smooth-nodes="true"
- inkscape:snap-midpoints="true"
- inkscape:snap-global="true">
- <sodipodi:guide
- position="10,10"
- orientation="1,0"
- id="guide4039"
- inkscape:locked="false" />
- <sodipodi:guide
- position="10,10"
- orientation="0,1"
- id="guide3935"
- inkscape:locked="false" />
- </sodipodi:namedview>
- <metadata
- id="metadata5">
- <rdf:RDF>
- <cc:Work
- rdf:about="">
- <dc:format>image/svg+xml</dc:format>
- <dc:type
- rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
- <dc:title />
- </cc:Work>
- </rdf:RDF>
- </metadata>
- <g
- inkscape:groupmode="layer"
- id="layer25"
- inkscape:label="Content">
- <g
- id="g3865">
- <path
- id="rect3928"
- d="m 0,14 v 3 3 h 20 v -3 -3 h -3 v 3 H 3 v -3 z"
- style="opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.40999997;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:normal"
- inkscape:connector-curvature="0" />
- <g
- id="g3859">
- <rect
- y="-12.83"
- x="7.85585e-016"
- height="5.6599998"
- width="13.981334"
- id="rect3930"
- style="opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.40999997;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:normal"
- transform="rotate(90)" />
- <path
- sodipodi:type="star"
- style="opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.83595276;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:normal"
- id="path3932"
- sodipodi:sides="3"
- sodipodi:cx="8.9146461"
- sodipodi:cy="9.684866"
- sodipodi:r1="4.0292788"
- sodipodi:r2="2.0146394"
- sodipodi:arg1="-1.5707963"
- sodipodi:arg2="-0.52359878"
- inkscape:flatsided="true"
- inkscape:rounded="0"
- inkscape:randomized="0"
- d="m 8.9146463,5.6555872 3.4894577,6.0439178 -6.9789157,0 z"
- inkscape:transform-center-y="-1.3333344"
- transform="matrix(-2.1493311,0,0,-1.3236446,29.160526,24.485987)" />
- </g>
- </g>
- </g>
-</svg>
diff --git a/src/stylesheets/Paper/Dark/restore.svg b/src/stylesheets/Paper/Dark/restore.svg
deleted file mode 100644
index 0511e182..00000000
--- a/src/stylesheets/Paper/Dark/restore.svg
+++ /dev/null
@@ -1,94 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
- xmlns:dc="http://purl.org/dc/elements/1.1/"
- xmlns:cc="http://creativecommons.org/ns#"
- xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
- xmlns:svg="http://www.w3.org/2000/svg"
- xmlns="http://www.w3.org/2000/svg"
- xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
- xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
- width="20"
- height="20"
- viewBox="0 0 20 20"
- version="1.1"
- id="svg8"
- inkscape:export-filename="D:\Bethesda Game Modding\Mod Organizer 2\stylesheets\Paper\Dark\dots.png"
- inkscape:export-xdpi="307.20001"
- inkscape:export-ydpi="307.20001"
- inkscape:version="0.92.1 r15371"
- sodipodi:docname="backup.svg">
- <defs
- id="defs2" />
- <sodipodi:namedview
- id="base"
- pagecolor="#505050"
- bordercolor="#666666"
- borderopacity="1.0"
- inkscape:pageopacity="0"
- inkscape:pageshadow="2"
- inkscape:zoom="22.59728"
- inkscape:cx="-3.3022115"
- inkscape:cy="8.6471788"
- inkscape:document-units="px"
- inkscape:current-layer="layer25"
- showgrid="false"
- units="px"
- borderlayer="true"
- inkscape:showpageshadow="false"
- inkscape:snap-page="true"
- inkscape:snap-bbox="true"
- inkscape:bbox-nodes="true"
- inkscape:snap-bbox-edge-midpoints="true"
- inkscape:snap-bbox-midpoints="true"
- showguides="true"
- inkscape:guide-bbox="true"
- inkscape:window-width="1920"
- inkscape:window-height="1027"
- inkscape:window-x="-8"
- inkscape:window-y="-8"
- inkscape:window-maximized="1"
- inkscape:snap-center="true"
- inkscape:snap-object-midpoints="true"
- inkscape:measure-start="18.8975,12.498"
- inkscape:measure-end="20,12.498"
- inkscape:snap-intersection-paths="true"
- inkscape:object-paths="true"
- inkscape:snap-smooth-nodes="true"
- inkscape:snap-midpoints="true"
- inkscape:snap-global="true">
- <sodipodi:guide
- position="10,10"
- orientation="1,0"
- id="guide4039"
- inkscape:locked="false" />
- <sodipodi:guide
- position="10,10"
- orientation="0,1"
- id="guide3935"
- inkscape:locked="false" />
- </sodipodi:namedview>
- <metadata
- id="metadata5">
- <rdf:RDF>
- <cc:Work
- rdf:about="">
- <dc:format>image/svg+xml</dc:format>
- <dc:type
- rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
- <dc:title />
- </cc:Work>
- </rdf:RDF>
- </metadata>
- <g
- inkscape:groupmode="layer"
- id="layer25"
- inkscape:label="Content">
- <path
- style="opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:3.00000024;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:stroke fill markers"
- d="m 10,-2.5e-7 a 10,10 0 0 1 0.998047,0.05078 A 10,10 0 0 1 19.787109,8.0019518 10,10 0 0 1 20,10 10,10 0 0 1 10,20 10,10 0 0 1 0.2128895,11.998047 h 3.082032 A 7,7 0 0 0 10,17 a 7,7 0 0 0 7,-7 7,7 0 0 0 -0.294922,-1.9980473 7,7 0 0 0 -0.53125,-1.291016 7,7 0 0 0 0,-0.0019 7,7 0 0 0 -0.359375,-0.599609 7,7 0 0 0 -0.417969,-0.5625 7,7 0 0 0 -0.40625,-0.447266 7,7 0 0 0 -0.07422,-0.07813 7,7 0 0 0 -0.435547,-0.390625 7,7 0 0 0 -0.08594,-0.07422 7,7 0 0 0 -0.490234,-0.359375 7,7 0 0 0 -0.07227,-0.05078 7,7 0 0 0 -0.49023,-0.289062 7,7 0 0 0 -0.119141,-0.06836 7,7 0 0 0 -0.439453,-0.203125 7,7 0 0 0 -0.207031,-0.0918 7,7 0 0 0 -0.402344,-0.138672 7,7 0 0 0 -0.259766,-0.08594 7,7 0 0 0 -0.466797,-0.109375 7,7 0 0 0 -0.208984,-0.04687 7,7 0 0 0 -0.458984,-0.06055 7,7 0 0 0 -0.246094,-0.03125 A 7,7 0 0 0 10,2.9999997 a 7,7 0 0 0 -2.06836,0.314453 7,7 0 0 0 -0.0019,0 7,7 0 0 0 -0.654297,0.240234 7,7 0 0 0 -0.0019,0 7,7 0 0 0 -0.628906,0.304688 7,7 0 0 0 -1.587891,1.197266 L 8,8.0019527 H 3.2968745 0.2031245 -5e-7 V -2.5e-7 l 2.933594,2.93359395 a 10,10 0 0 1 0.01172,-0.01172 10,10 0 0 1 0.591797,-0.535156 10,10 0 0 1 0.169922,-0.148438 10,10 0 0 1 0.6015635,-0.447265 10,10 0 0 1 0.199218,-0.142579 10,10 0 0 1 0.644532,-0.380859 10,10 0 0 1 0.222656,-0.126953 10,10 0 0 1 0.632812,-0.29492195 10,10 0 0 1 0.283204,-0.126953 10,10 0 0 1 0.623046,-0.216797 10,10 0 0 1 0.328125,-0.109375 10,10 0 0 1 0.601563,-0.144531 10,10 0 0 1 0.375,-0.08398 10,10 0 0 1 0.603516,-0.08203 10,10 0 0 1 0.394531,-0.04883 A 10,10 0 0 1 10,-2.5e-7 Z"
- id="path3956"
- inkscape:connector-curvature="0" />
- </g>
-</svg>
diff --git a/src/stylesheets/Paper/Dark/run.svg b/src/stylesheets/Paper/Dark/run.svg
deleted file mode 100644
index 008b2ccc..00000000
--- a/src/stylesheets/Paper/Dark/run.svg
+++ /dev/null
@@ -1,109 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
- xmlns:dc="http://purl.org/dc/elements/1.1/"
- xmlns:cc="http://creativecommons.org/ns#"
- xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
- xmlns:svg="http://www.w3.org/2000/svg"
- xmlns="http://www.w3.org/2000/svg"
- xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
- xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
- width="128"
- height="128"
- viewBox="0 0 128 128"
- version="1.1"
- id="svg8"
- inkscape:export-filename="D:\Bethesda Game Modding\Mod Organizer 2\stylesheets\Paper\Dark\dots.png"
- inkscape:export-xdpi="307.20001"
- inkscape:export-ydpi="307.20001"
- inkscape:version="0.92.1 r15371"
- sodipodi:docname="run.svg">
- <defs
- id="defs2" />
- <sodipodi:namedview
- id="base"
- pagecolor="#505050"
- bordercolor="#666666"
- borderopacity="1.0"
- inkscape:pageopacity="0"
- inkscape:pageshadow="2"
- inkscape:zoom="3.0446864"
- inkscape:cx="23.34915"
- inkscape:cy="69.793752"
- inkscape:document-units="px"
- inkscape:current-layer="layer25"
- showgrid="false"
- units="px"
- borderlayer="true"
- inkscape:showpageshadow="false"
- inkscape:snap-page="true"
- inkscape:snap-bbox="true"
- inkscape:bbox-nodes="true"
- inkscape:snap-bbox-edge-midpoints="true"
- inkscape:snap-bbox-midpoints="true"
- showguides="true"
- inkscape:guide-bbox="true"
- inkscape:window-width="1920"
- inkscape:window-height="1027"
- inkscape:window-x="-8"
- inkscape:window-y="-8"
- inkscape:window-maximized="1"
- inkscape:snap-center="true"
- inkscape:snap-object-midpoints="true"
- inkscape:measure-start="18.8975,12.498"
- inkscape:measure-end="20,12.498"
- inkscape:snap-intersection-paths="true"
- inkscape:object-paths="true"
- inkscape:snap-smooth-nodes="true"
- inkscape:snap-midpoints="true"
- inkscape:snap-global="true">
- <sodipodi:guide
- position="64,64"
- orientation="1,0"
- id="guide4039"
- inkscape:locked="false" />
- <sodipodi:guide
- position="64,64"
- orientation="0,1"
- id="guide3935"
- inkscape:locked="false"
- inkscape:label=""
- inkscape:color="rgb(0,0,255)" />
- </sodipodi:namedview>
- <metadata
- id="metadata5">
- <rdf:RDF>
- <cc:Work
- rdf:about="">
- <dc:format>image/svg+xml</dc:format>
- <dc:type
- rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
- <dc:title></dc:title>
- </cc:Work>
- </rdf:RDF>
- </metadata>
- <g
- inkscape:groupmode="layer"
- id="layer25"
- inkscape:label="Content"
- transform="translate(0,108)">
- <path
- sodipodi:type="star"
- style="display:inline;opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.99999994;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:normal"
- id="path3868"
- sodipodi:sides="3"
- sodipodi:cx="63.999996"
- sodipodi:cy="-43.999996"
- sodipodi:r1="64.000008"
- sodipodi:r2="32.000004"
- sodipodi:arg1="0"
- sodipodi:arg2="1.0471976"
- inkscape:flatsided="true"
- inkscape:rounded="0"
- inkscape:randomized="0"
- d="m 128,-43.999996 -96.000008,55.425632 0,-110.851265 z"
- inkscape:transform-center-x="-15.999997"
- inkscape:transform-center-y="3.8146973e-006" />
- </g>
-</svg>
diff --git a/src/stylesheets/Paper/Dark/shortcut.svg b/src/stylesheets/Paper/Dark/shortcut.svg
deleted file mode 100644
index 27a3497a..00000000
--- a/src/stylesheets/Paper/Dark/shortcut.svg
+++ /dev/null
@@ -1,96 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
- xmlns:dc="http://purl.org/dc/elements/1.1/"
- xmlns:cc="http://creativecommons.org/ns#"
- xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
- xmlns:svg="http://www.w3.org/2000/svg"
- xmlns="http://www.w3.org/2000/svg"
- xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
- xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
- width="20"
- height="20"
- viewBox="0 0 20 20"
- version="1.1"
- id="svg8"
- inkscape:export-filename="D:\Bethesda Game Modding\Mod Organizer 2\stylesheets\Paper\Dark\dots.png"
- inkscape:export-xdpi="307.20001"
- inkscape:export-ydpi="307.20001"
- inkscape:version="0.92.1 r15371"
- sodipodi:docname="shortcut.svg">
- <defs
- id="defs2" />
- <sodipodi:namedview
- id="base"
- pagecolor="#505050"
- bordercolor="#666666"
- borderopacity="1.0"
- inkscape:pageopacity="0"
- inkscape:pageshadow="2"
- inkscape:zoom="12.178745"
- inkscape:cx="16.395211"
- inkscape:cy="11.770603"
- inkscape:document-units="px"
- inkscape:current-layer="layer25"
- showgrid="false"
- units="px"
- borderlayer="true"
- inkscape:showpageshadow="false"
- inkscape:snap-page="true"
- inkscape:snap-bbox="true"
- inkscape:bbox-nodes="true"
- inkscape:snap-bbox-edge-midpoints="true"
- inkscape:snap-bbox-midpoints="true"
- showguides="true"
- inkscape:guide-bbox="true"
- inkscape:window-width="1920"
- inkscape:window-height="1027"
- inkscape:window-x="-8"
- inkscape:window-y="-8"
- inkscape:window-maximized="1"
- inkscape:snap-center="true"
- inkscape:snap-object-midpoints="true"
- inkscape:measure-start="18.8975,12.498"
- inkscape:measure-end="20,12.498"
- inkscape:snap-intersection-paths="true"
- inkscape:object-paths="true"
- inkscape:snap-smooth-nodes="true"
- inkscape:snap-midpoints="true"
- inkscape:snap-global="true">
- <sodipodi:guide
- position="10,10"
- orientation="1,0"
- id="guide4039"
- inkscape:locked="false" />
- <sodipodi:guide
- position="10,10"
- orientation="0,1"
- id="guide3935"
- inkscape:locked="false" />
- </sodipodi:namedview>
- <metadata
- id="metadata5">
- <rdf:RDF>
- <cc:Work
- rdf:about="">
- <dc:format>image/svg+xml</dc:format>
- <dc:type
- rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
- <dc:title></dc:title>
- </cc:Work>
- </rdf:RDF>
- </metadata>
- <g
- inkscape:groupmode="layer"
- id="layer25"
- inkscape:label="Content">
- <path
- inkscape:connector-curvature="0"
- style="display:inline;opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:3;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:stroke fill markers"
- d="M 12,1.1035164 V 5.4492192 A 9.7951658,7.8360983 0 0 0 2.2050794,13.285155 9.7951658,7.8360983 0 0 0 6.986329,19.999998 H 8.2011726 A 5.8526121,4.682069 0 0 1 6.1484379,16.437499 5.8526121,4.682069 0 0 1 12,11.757811 v 4.345703 l 8,-7.4999984 z"
- id="rect3929"
- inkscape:transform-center-x="-1.1025401"
- inkscape:transform-center-y="0.5517574" />
- </g>
-</svg>
diff --git a/src/stylesheets/Paper/Dark/sort.svg b/src/stylesheets/Paper/Dark/sort.svg
deleted file mode 100644
index 372bc1cd..00000000
--- a/src/stylesheets/Paper/Dark/sort.svg
+++ /dev/null
@@ -1,122 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
- xmlns:dc="http://purl.org/dc/elements/1.1/"
- xmlns:cc="http://creativecommons.org/ns#"
- xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
- xmlns:svg="http://www.w3.org/2000/svg"
- xmlns="http://www.w3.org/2000/svg"
- xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
- xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
- width="20"
- height="20"
- viewBox="0 0 20 20"
- version="1.1"
- id="svg8"
- inkscape:export-filename="D:\Bethesda Game Modding\Mod Organizer 2\stylesheets\Paper\Dark\dots.png"
- inkscape:export-xdpi="307.20001"
- inkscape:export-ydpi="307.20001"
- inkscape:version="0.92.1 r15371"
- sodipodi:docname="sort.svg">
- <defs
- id="defs2" />
- <sodipodi:namedview
- id="base"
- pagecolor="#505050"
- bordercolor="#666666"
- borderopacity="1.0"
- inkscape:pageopacity="0"
- inkscape:pageshadow="2"
- inkscape:zoom="26.25481"
- inkscape:cx="2.1341235"
- inkscape:cy="4.6849693"
- inkscape:document-units="px"
- inkscape:current-layer="layer25"
- showgrid="false"
- units="px"
- borderlayer="true"
- inkscape:showpageshadow="false"
- inkscape:snap-page="true"
- inkscape:snap-bbox="true"
- inkscape:bbox-nodes="true"
- inkscape:snap-bbox-edge-midpoints="true"
- inkscape:snap-bbox-midpoints="true"
- showguides="true"
- inkscape:guide-bbox="true"
- inkscape:window-width="1920"
- inkscape:window-height="1027"
- inkscape:window-x="-8"
- inkscape:window-y="-8"
- inkscape:window-maximized="1"
- inkscape:snap-center="true"
- inkscape:snap-object-midpoints="true"
- inkscape:measure-start="18.8975,12.498"
- inkscape:measure-end="20,12.498"
- inkscape:snap-intersection-paths="true"
- inkscape:object-paths="true"
- inkscape:snap-smooth-nodes="true"
- inkscape:snap-midpoints="true"
- inkscape:snap-global="true">
- <sodipodi:guide
- position="10,10"
- orientation="1,0"
- id="guide4039"
- inkscape:locked="false" />
- <sodipodi:guide
- position="10,10"
- orientation="0,1"
- id="guide3935"
- inkscape:locked="false" />
- </sodipodi:namedview>
- <metadata
- id="metadata5">
- <rdf:RDF>
- <cc:Work
- rdf:about="">
- <dc:format>image/svg+xml</dc:format>
- <dc:type
- rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
- <dc:title></dc:title>
- </cc:Work>
- </rdf:RDF>
- </metadata>
- <g
- inkscape:groupmode="layer"
- id="layer25"
- inkscape:label="Content">
- <g
- id="g3898">
- <g
- id="g3886">
- <rect
- y="4"
- x="13"
- height="16"
- width="3"
- id="rect4003"
- style="display:inline;opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.40999997;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:normal" />
- <path
- style="display:inline;opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.40999997;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:normal"
- d="m 13,0 v 7.9999998 h 7 z"
- id="path4009"
- inkscape:connector-curvature="0" />
- </g>
- <g
- id="g3890">
- <rect
- style="display:inline;opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.40999997;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:normal"
- id="rect3999"
- width="3"
- height="16"
- x="4"
- y="2.2775268e-008" />
- <path
- inkscape:connector-curvature="0"
- id="path4016"
- d="M 7,20 V 12 H 0 Z"
- style="display:inline;opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.40999997;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:normal" />
- </g>
- </g>
- </g>
-</svg>
diff --git a/src/stylesheets/Paper/Dark/unchecked-alt.svg b/src/stylesheets/Paper/Dark/unchecked-alt.svg
deleted file mode 100644
index 4f5bbd04..00000000
--- a/src/stylesheets/Paper/Dark/unchecked-alt.svg
+++ /dev/null
@@ -1,100 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
- xmlns:dc="http://purl.org/dc/elements/1.1/"
- xmlns:cc="http://creativecommons.org/ns#"
- xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
- xmlns:svg="http://www.w3.org/2000/svg"
- xmlns="http://www.w3.org/2000/svg"
- xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
- xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
- width="20"
- height="20"
- viewBox="0 0 20 20"
- version="1.1"
- id="svg8"
- inkscape:export-filename="D:\Bethesda Game Modding\Mod Organizer 2\stylesheets\Paper\Dark\dots.png"
- inkscape:export-xdpi="307.20001"
- inkscape:export-ydpi="307.20001"
- inkscape:version="0.92.1 r15371"
- sodipodi:docname="unchecked-alt.svg">
- <defs
- id="defs2" />
- <sodipodi:namedview
- id="base"
- pagecolor="#505050"
- bordercolor="#666666"
- borderopacity="1.0"
- inkscape:pageopacity="0"
- inkscape:pageshadow="2"
- inkscape:zoom="16"
- inkscape:cx="-3.1093521"
- inkscape:cy="-9.0242346"
- inkscape:document-units="px"
- inkscape:current-layer="layer25"
- showgrid="false"
- units="px"
- borderlayer="true"
- inkscape:showpageshadow="false"
- inkscape:snap-page="true"
- inkscape:snap-bbox="true"
- inkscape:bbox-nodes="true"
- inkscape:snap-bbox-edge-midpoints="true"
- inkscape:snap-bbox-midpoints="true"
- showguides="true"
- inkscape:guide-bbox="true"
- inkscape:window-width="1920"
- inkscape:window-height="1027"
- inkscape:window-x="-8"
- inkscape:window-y="-8"
- inkscape:window-maximized="1"
- inkscape:snap-center="true"
- inkscape:snap-object-midpoints="true"
- inkscape:measure-start="18.8975,12.498"
- inkscape:measure-end="20,12.498"
- inkscape:snap-intersection-paths="true"
- inkscape:object-paths="true"
- inkscape:snap-smooth-nodes="true"
- inkscape:snap-midpoints="true"
- inkscape:snap-global="true">
- <sodipodi:guide
- position="10,10"
- orientation="1,0"
- id="guide4039"
- inkscape:locked="false" />
- <sodipodi:guide
- position="10,10"
- orientation="0,1"
- id="guide3935"
- inkscape:locked="false" />
- </sodipodi:namedview>
- <metadata
- id="metadata5">
- <rdf:RDF>
- <cc:Work
- rdf:about="">
- <dc:format>image/svg+xml</dc:format>
- <dc:type
- rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
- <dc:title />
- </cc:Work>
- </rdf:RDF>
- </metadata>
- <g
- inkscape:groupmode="layer"
- id="layer25"
- inkscape:label="Content">
- <path
- inkscape:connector-curvature="0"
- style="display:inline;opacity:1;fill:#fafafa;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:1.93939996;stroke-opacity:1;paint-order:markers stroke fill"
- d="M 10,2.7e-7 C 4.4771523,2.7e-7 3e-7,4.4771526 3e-7,10 3e-7,15.522848 4.4771523,20 10,20 15.522848,20 20,15.522848 20,10 20,4.4771526 15.522848,2.7e-7 10,2.7e-7 Z m 0,2.49999983 c 4.142136,0 7.5,3.3578645 7.5,7.4999999 0,4.142136 -3.357864,7.5 -7.5,7.5 C 5.8578643,17.5 2.5000003,14.142136 2.5000003,10 2.5000003,5.8578646 5.8578643,2.5000001 10,2.5000001 Z"
- id="path3872-6" />
- <path
- style="display:inline;opacity:1;fill:#007e7e;fill-opacity:0;fill-rule:nonzero;stroke:none;stroke-width:4;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:1.93939996;stroke-opacity:1;paint-order:markers stroke fill"
- d="m 10,2 c -4.418278,0 -8,3.581722 -8,8 0,4.418278 3.581722,8 8,8 4.418278,0 8,-3.581722 8,-8 0,-4.418278 -3.581722,-8 -8,-8 z"
- id="path3872"
- inkscape:connector-curvature="0"
- sodipodi:nodetypes="sssss" />
- </g>
-</svg>
diff --git a/src/stylesheets/Paper/Dark/unchecked-disabled.svg b/src/stylesheets/Paper/Dark/unchecked-disabled.svg
deleted file mode 100644
index befdeba1..00000000
--- a/src/stylesheets/Paper/Dark/unchecked-disabled.svg
+++ /dev/null
@@ -1,94 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
- xmlns:dc="http://purl.org/dc/elements/1.1/"
- xmlns:cc="http://creativecommons.org/ns#"
- xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
- xmlns:svg="http://www.w3.org/2000/svg"
- xmlns="http://www.w3.org/2000/svg"
- xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
- xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
- width="20"
- height="20"
- viewBox="0 0 20 20"
- version="1.1"
- id="svg8"
- inkscape:export-filename="D:\Bethesda Game Modding\Mod Organizer 2\stylesheets\Paper\Dark\dots.png"
- inkscape:export-xdpi="307.20001"
- inkscape:export-ydpi="307.20001"
- inkscape:version="0.92.1 r15371"
- sodipodi:docname="unchecked-disabled.svg">
- <defs
- id="defs2" />
- <sodipodi:namedview
- id="base"
- pagecolor="#505050"
- bordercolor="#666666"
- borderopacity="1.0"
- inkscape:pageopacity="0"
- inkscape:pageshadow="2"
- inkscape:zoom="27.505434"
- inkscape:cx="6.0413321"
- inkscape:cy="6.6331774"
- inkscape:document-units="px"
- inkscape:current-layer="layer25"
- showgrid="false"
- units="px"
- borderlayer="true"
- inkscape:showpageshadow="false"
- inkscape:snap-page="true"
- inkscape:snap-bbox="true"
- inkscape:bbox-nodes="true"
- inkscape:snap-bbox-edge-midpoints="true"
- inkscape:snap-bbox-midpoints="true"
- showguides="true"
- inkscape:guide-bbox="true"
- inkscape:window-width="1920"
- inkscape:window-height="1027"
- inkscape:window-x="-8"
- inkscape:window-y="-8"
- inkscape:window-maximized="1"
- inkscape:snap-center="true"
- inkscape:snap-object-midpoints="true"
- inkscape:measure-start="18.8975,12.498"
- inkscape:measure-end="20,12.498"
- inkscape:snap-intersection-paths="true"
- inkscape:object-paths="true"
- inkscape:snap-smooth-nodes="true"
- inkscape:snap-midpoints="true"
- inkscape:snap-global="true">
- <sodipodi:guide
- position="10,10"
- orientation="1,0"
- id="guide4039"
- inkscape:locked="false" />
- <sodipodi:guide
- position="10,10"
- orientation="0,1"
- id="guide3935"
- inkscape:locked="false" />
- </sodipodi:namedview>
- <metadata
- id="metadata5">
- <rdf:RDF>
- <cc:Work
- rdf:about="">
- <dc:format>image/svg+xml</dc:format>
- <dc:type
- rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
- <dc:title />
- </cc:Work>
- </rdf:RDF>
- </metadata>
- <g
- inkscape:groupmode="layer"
- id="layer25"
- inkscape:label="Content">
- <path
- style="display:inline;opacity:1;fill:#6c6c6c;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:169.70561218;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:1.93939996;stroke-opacity:1;paint-order:markers stroke fill"
- d="M 4.0000001,-1.65e-7 1.2e-7,3.9999998 l 1.99999998,2 4,3.9999994 -4,4.0000008 -1.99999998,2 3.99999998,4 2,-2 L 10,14 l 4,4 2,2 4,-4 -2,-2 -4,-4.0000008 4,-3.9999994 2,-2 L 16,-1.65e-7 14,1.9999999 10,5.9999998 6.0000001,1.9999999 Z"
- id="path3944"
- inkscape:connector-curvature="0" />
- </g>
-</svg>
diff --git a/src/stylesheets/Paper/Dark/unchecked.svg b/src/stylesheets/Paper/Dark/unchecked.svg
deleted file mode 100644
index 41c91aa6..00000000
--- a/src/stylesheets/Paper/Dark/unchecked.svg
+++ /dev/null
@@ -1,100 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
- xmlns:dc="http://purl.org/dc/elements/1.1/"
- xmlns:cc="http://creativecommons.org/ns#"
- xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
- xmlns:svg="http://www.w3.org/2000/svg"
- xmlns="http://www.w3.org/2000/svg"
- xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
- xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
- width="20"
- height="20"
- viewBox="0 0 20 20"
- version="1.1"
- id="svg8"
- inkscape:export-filename="D:\Bethesda Game Modding\Mod Organizer 2\stylesheets\Paper\Dark\dots.png"
- inkscape:export-xdpi="307.20001"
- inkscape:export-ydpi="307.20001"
- inkscape:version="0.92.1 r15371"
- sodipodi:docname="unchecked.svg">
- <defs
- id="defs2" />
- <sodipodi:namedview
- id="base"
- pagecolor="#505050"
- bordercolor="#666666"
- borderopacity="1.0"
- inkscape:pageopacity="0"
- inkscape:pageshadow="2"
- inkscape:zoom="36.087529"
- inkscape:cx="8.2851418"
- inkscape:cy="9.2482234"
- inkscape:document-units="px"
- inkscape:current-layer="layer25"
- showgrid="false"
- units="px"
- borderlayer="true"
- inkscape:showpageshadow="false"
- inkscape:snap-page="true"
- inkscape:snap-bbox="true"
- inkscape:bbox-nodes="true"
- inkscape:snap-bbox-edge-midpoints="true"
- inkscape:snap-bbox-midpoints="true"
- showguides="true"
- inkscape:guide-bbox="true"
- inkscape:window-width="1920"
- inkscape:window-height="1027"
- inkscape:window-x="-8"
- inkscape:window-y="-8"
- inkscape:window-maximized="1"
- inkscape:snap-center="true"
- inkscape:snap-object-midpoints="true"
- inkscape:measure-start="18.8975,12.498"
- inkscape:measure-end="20,12.498"
- inkscape:snap-intersection-paths="true"
- inkscape:object-paths="true"
- inkscape:snap-smooth-nodes="true"
- inkscape:snap-midpoints="true"
- inkscape:snap-global="true">
- <sodipodi:guide
- position="10,10"
- orientation="1,0"
- id="guide4039"
- inkscape:locked="false" />
- <sodipodi:guide
- position="10,10"
- orientation="0,1"
- id="guide3935"
- inkscape:locked="false" />
- </sodipodi:namedview>
- <metadata
- id="metadata5">
- <rdf:RDF>
- <cc:Work
- rdf:about="">
- <dc:format>image/svg+xml</dc:format>
- <dc:type
- rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
- <dc:title />
- </cc:Work>
- </rdf:RDF>
- </metadata>
- <g
- inkscape:groupmode="layer"
- id="layer25"
- inkscape:label="Content">
- <path
- inkscape:connector-curvature="0"
- style="display:inline;opacity:1;fill:#d3d3d3;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:1.93939996;stroke-opacity:1;paint-order:markers stroke fill"
- d="M 10,2.7e-7 C 4.4771523,2.7e-7 3e-7,4.4771526 3e-7,10 3e-7,15.522848 4.4771523,20 10,20 15.522848,20 20,15.522848 20,10 20,4.4771526 15.522848,2.7e-7 10,2.7e-7 Z m 0,2.49999983 c 4.142136,0 7.5,3.3578645 7.5,7.4999999 0,4.142136 -3.357864,7.5 -7.5,7.5 C 5.8578643,17.5 2.5000003,14.142136 2.5000003,10 2.5000003,5.8578646 5.8578643,2.5000001 10,2.5000001 Z"
- id="path3872-6" />
- <path
- style="display:inline;opacity:1;fill:#007e7e;fill-opacity:0;fill-rule:nonzero;stroke:none;stroke-width:4;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:1.93939996;stroke-opacity:1;paint-order:markers stroke fill"
- d="m 10,2 c -4.418278,0 -8,3.581722 -8,8 0,4.418278 3.581722,8 8,8 4.418278,0 8,-3.581722 8,-8 0,-4.418278 -3.581722,-8 -8,-8 z"
- id="path3872"
- inkscape:connector-curvature="0"
- sodipodi:nodetypes="sssss" />
- </g>
-</svg>
diff --git a/src/stylesheets/Paper/Light/Arrows/down.svg b/src/stylesheets/Paper/Light/Arrows/down.svg
deleted file mode 100644
index 106b64a5..00000000
--- a/src/stylesheets/Paper/Light/Arrows/down.svg
+++ /dev/null
@@ -1,97 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
- xmlns:dc="http://purl.org/dc/elements/1.1/"
- xmlns:cc="http://creativecommons.org/ns#"
- xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
- xmlns:svg="http://www.w3.org/2000/svg"
- xmlns="http://www.w3.org/2000/svg"
- xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
- xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
- width="8"
- height="8"
- viewBox="0 0 8 8"
- version="1.1"
- id="svg8"
- inkscape:export-filename="D:\Bethesda Game Modding\Mod Organizer 2\stylesheets\Paper\Dark\dots.png"
- inkscape:export-xdpi="307.20001"
- inkscape:export-ydpi="307.20001"
- inkscape:version="0.92.1 r15371"
- sodipodi:docname="down.svg">
- <defs
- id="defs2" />
- <sodipodi:namedview
- id="base"
- pagecolor="#505050"
- bordercolor="#666666"
- borderopacity="1.0"
- inkscape:pageopacity="0"
- inkscape:pageshadow="2"
- inkscape:zoom="97.429959"
- inkscape:cx="-1.2019503"
- inkscape:cy="5.6881881"
- inkscape:document-units="px"
- inkscape:current-layer="layer25"
- showgrid="false"
- units="px"
- borderlayer="true"
- inkscape:showpageshadow="false"
- inkscape:snap-page="true"
- inkscape:snap-bbox="true"
- inkscape:bbox-nodes="true"
- inkscape:snap-bbox-edge-midpoints="true"
- inkscape:snap-bbox-midpoints="true"
- showguides="true"
- inkscape:guide-bbox="true"
- inkscape:window-width="1920"
- inkscape:window-height="1027"
- inkscape:window-x="-8"
- inkscape:window-y="-8"
- inkscape:window-maximized="1"
- inkscape:snap-center="true"
- inkscape:snap-object-midpoints="true"
- inkscape:measure-start="18.8975,12.498"
- inkscape:measure-end="20,12.498"
- inkscape:snap-intersection-paths="true"
- inkscape:object-paths="true"
- inkscape:snap-smooth-nodes="true"
- inkscape:snap-midpoints="true"
- inkscape:snap-global="true">
- <sodipodi:guide
- position="4,4"
- orientation="1,0"
- id="guide4039"
- inkscape:locked="false" />
- <sodipodi:guide
- position="4,4"
- orientation="0,1"
- id="guide3935"
- inkscape:locked="false"
- inkscape:label=""
- inkscape:color="rgb(0,0,255)" />
- </sodipodi:namedview>
- <metadata
- id="metadata5">
- <rdf:RDF>
- <cc:Work
- rdf:about="">
- <dc:format>image/svg+xml</dc:format>
- <dc:type
- rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
- <dc:title />
- </cc:Work>
- </rdf:RDF>
- </metadata>
- <g
- inkscape:groupmode="layer"
- id="layer25"
- inkscape:label="Content"
- transform="translate(0,-12)">
- <path
- style="display:inline;opacity:1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:212.1320343;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:1.93939996;stroke-opacity:1;paint-order:markers stroke fill"
- d="M 0,15 2,17 4,19 6,17 8,15 6,13 4,15 2,13 Z"
- id="rect3900-0"
- inkscape:connector-curvature="0" />
- </g>
-</svg>
diff --git a/src/stylesheets/Paper/Light/Arrows/left.svg b/src/stylesheets/Paper/Light/Arrows/left.svg
deleted file mode 100644
index 063012ef..00000000
--- a/src/stylesheets/Paper/Light/Arrows/left.svg
+++ /dev/null
@@ -1,97 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
- xmlns:dc="http://purl.org/dc/elements/1.1/"
- xmlns:cc="http://creativecommons.org/ns#"
- xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
- xmlns:svg="http://www.w3.org/2000/svg"
- xmlns="http://www.w3.org/2000/svg"
- xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
- xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
- width="8"
- height="8"
- viewBox="0 0 8 8"
- version="1.1"
- id="svg8"
- inkscape:export-filename="D:\Bethesda Game Modding\Mod Organizer 2\stylesheets\Paper\Dark\dots.png"
- inkscape:export-xdpi="307.20001"
- inkscape:export-ydpi="307.20001"
- inkscape:version="0.92.1 r15371"
- sodipodi:docname="left.svg">
- <defs
- id="defs2" />
- <sodipodi:namedview
- id="base"
- pagecolor="#505050"
- bordercolor="#666666"
- borderopacity="1.0"
- inkscape:pageopacity="0"
- inkscape:pageshadow="2"
- inkscape:zoom="97.429959"
- inkscape:cx="-1.2019503"
- inkscape:cy="5.6881881"
- inkscape:document-units="px"
- inkscape:current-layer="layer25"
- showgrid="false"
- units="px"
- borderlayer="true"
- inkscape:showpageshadow="false"
- inkscape:snap-page="true"
- inkscape:snap-bbox="true"
- inkscape:bbox-nodes="true"
- inkscape:snap-bbox-edge-midpoints="true"
- inkscape:snap-bbox-midpoints="true"
- showguides="true"
- inkscape:guide-bbox="true"
- inkscape:window-width="1920"
- inkscape:window-height="1027"
- inkscape:window-x="-8"
- inkscape:window-y="-8"
- inkscape:window-maximized="1"
- inkscape:snap-center="true"
- inkscape:snap-object-midpoints="true"
- inkscape:measure-start="18.8975,12.498"
- inkscape:measure-end="20,12.498"
- inkscape:snap-intersection-paths="true"
- inkscape:object-paths="true"
- inkscape:snap-smooth-nodes="true"
- inkscape:snap-midpoints="true"
- inkscape:snap-global="true">
- <sodipodi:guide
- position="4,4"
- orientation="1,0"
- id="guide4039"
- inkscape:locked="false" />
- <sodipodi:guide
- position="4,4"
- orientation="0,1"
- id="guide3935"
- inkscape:locked="false"
- inkscape:label=""
- inkscape:color="rgb(0,0,255)" />
- </sodipodi:namedview>
- <metadata
- id="metadata5">
- <rdf:RDF>
- <cc:Work
- rdf:about="">
- <dc:format>image/svg+xml</dc:format>
- <dc:type
- rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
- <dc:title />
- </cc:Work>
- </rdf:RDF>
- </metadata>
- <g
- inkscape:groupmode="layer"
- id="layer25"
- inkscape:label="Content"
- transform="translate(0,-12)">
- <path
- style="display:inline;opacity:1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:212.1320343;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:1.93939996;stroke-opacity:1;paint-order:markers stroke fill"
- d="M 5,20 3,18 1,16 3,14 5,12 7,14 5,16 7,18 Z"
- id="rect3900-0"
- inkscape:connector-curvature="0" />
- </g>
-</svg>
diff --git a/src/stylesheets/Paper/Light/Arrows/right.svg b/src/stylesheets/Paper/Light/Arrows/right.svg
deleted file mode 100644
index 21d222e2..00000000
--- a/src/stylesheets/Paper/Light/Arrows/right.svg
+++ /dev/null
@@ -1,97 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
- xmlns:dc="http://purl.org/dc/elements/1.1/"
- xmlns:cc="http://creativecommons.org/ns#"
- xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
- xmlns:svg="http://www.w3.org/2000/svg"
- xmlns="http://www.w3.org/2000/svg"
- xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
- xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
- width="8"
- height="8"
- viewBox="0 0 8 8"
- version="1.1"
- id="svg8"
- inkscape:export-filename="D:\Bethesda Game Modding\Mod Organizer 2\stylesheets\Paper\Dark\dots.png"
- inkscape:export-xdpi="307.20001"
- inkscape:export-ydpi="307.20001"
- inkscape:version="0.92.1 r15371"
- sodipodi:docname="right.svg">
- <defs
- id="defs2" />
- <sodipodi:namedview
- id="base"
- pagecolor="#505050"
- bordercolor="#666666"
- borderopacity="1.0"
- inkscape:pageopacity="0"
- inkscape:pageshadow="2"
- inkscape:zoom="97.429959"
- inkscape:cx="-1.2019503"
- inkscape:cy="5.6881881"
- inkscape:document-units="px"
- inkscape:current-layer="layer25"
- showgrid="false"
- units="px"
- borderlayer="true"
- inkscape:showpageshadow="false"
- inkscape:snap-page="true"
- inkscape:snap-bbox="true"
- inkscape:bbox-nodes="true"
- inkscape:snap-bbox-edge-midpoints="true"
- inkscape:snap-bbox-midpoints="true"
- showguides="true"
- inkscape:guide-bbox="true"
- inkscape:window-width="1920"
- inkscape:window-height="1027"
- inkscape:window-x="-8"
- inkscape:window-y="-8"
- inkscape:window-maximized="1"
- inkscape:snap-center="true"
- inkscape:snap-object-midpoints="true"
- inkscape:measure-start="18.8975,12.498"
- inkscape:measure-end="20,12.498"
- inkscape:snap-intersection-paths="true"
- inkscape:object-paths="true"
- inkscape:snap-smooth-nodes="true"
- inkscape:snap-midpoints="true"
- inkscape:snap-global="true">
- <sodipodi:guide
- position="4,4"
- orientation="1,0"
- id="guide4039"
- inkscape:locked="false" />
- <sodipodi:guide
- position="4,4"
- orientation="0,1"
- id="guide3935"
- inkscape:locked="false"
- inkscape:label=""
- inkscape:color="rgb(0,0,255)" />
- </sodipodi:namedview>
- <metadata
- id="metadata5">
- <rdf:RDF>
- <cc:Work
- rdf:about="">
- <dc:format>image/svg+xml</dc:format>
- <dc:type
- rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
- <dc:title />
- </cc:Work>
- </rdf:RDF>
- </metadata>
- <g
- inkscape:groupmode="layer"
- id="layer25"
- inkscape:label="Content"
- transform="translate(0,-12)">
- <path
- style="display:inline;opacity:1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:212.1320343;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:1.93939996;stroke-opacity:1;paint-order:markers stroke fill"
- d="M 3,20 5,18 7,16 5,14 3,12 1,14 3,16 1,18 Z"
- id="rect3900-0"
- inkscape:connector-curvature="0" />
- </g>
-</svg>
diff --git a/src/stylesheets/Paper/Light/Arrows/up.svg b/src/stylesheets/Paper/Light/Arrows/up.svg
deleted file mode 100644
index 967f3185..00000000
--- a/src/stylesheets/Paper/Light/Arrows/up.svg
+++ /dev/null
@@ -1,97 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
- xmlns:dc="http://purl.org/dc/elements/1.1/"
- xmlns:cc="http://creativecommons.org/ns#"
- xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
- xmlns:svg="http://www.w3.org/2000/svg"
- xmlns="http://www.w3.org/2000/svg"
- xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
- xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
- width="8"
- height="8"
- viewBox="0 0 8 8"
- version="1.1"
- id="svg8"
- inkscape:export-filename="D:\Bethesda Game Modding\Mod Organizer 2\stylesheets\Paper\Dark\dots.png"
- inkscape:export-xdpi="307.20001"
- inkscape:export-ydpi="307.20001"
- inkscape:version="0.92.1 r15371"
- sodipodi:docname="up.svg">
- <defs
- id="defs2" />
- <sodipodi:namedview
- id="base"
- pagecolor="#505050"
- bordercolor="#666666"
- borderopacity="1.0"
- inkscape:pageopacity="0"
- inkscape:pageshadow="2"
- inkscape:zoom="97.429959"
- inkscape:cx="-1.2019503"
- inkscape:cy="5.6881881"
- inkscape:document-units="px"
- inkscape:current-layer="layer25"
- showgrid="false"
- units="px"
- borderlayer="true"
- inkscape:showpageshadow="false"
- inkscape:snap-page="true"
- inkscape:snap-bbox="true"
- inkscape:bbox-nodes="true"
- inkscape:snap-bbox-edge-midpoints="true"
- inkscape:snap-bbox-midpoints="true"
- showguides="true"
- inkscape:guide-bbox="true"
- inkscape:window-width="1920"
- inkscape:window-height="1027"
- inkscape:window-x="-8"
- inkscape:window-y="-8"
- inkscape:window-maximized="1"
- inkscape:snap-center="true"
- inkscape:snap-object-midpoints="true"
- inkscape:measure-start="18.8975,12.498"
- inkscape:measure-end="20,12.498"
- inkscape:snap-intersection-paths="true"
- inkscape:object-paths="true"
- inkscape:snap-smooth-nodes="true"
- inkscape:snap-midpoints="true"
- inkscape:snap-global="true">
- <sodipodi:guide
- position="4,4"
- orientation="1,0"
- id="guide4039"
- inkscape:locked="false" />
- <sodipodi:guide
- position="4,4"
- orientation="0,1"
- id="guide3935"
- inkscape:locked="false"
- inkscape:label=""
- inkscape:color="rgb(0,0,255)" />
- </sodipodi:namedview>
- <metadata
- id="metadata5">
- <rdf:RDF>
- <cc:Work
- rdf:about="">
- <dc:format>image/svg+xml</dc:format>
- <dc:type
- rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
- <dc:title />
- </cc:Work>
- </rdf:RDF>
- </metadata>
- <g
- inkscape:groupmode="layer"
- id="layer25"
- inkscape:label="Content"
- transform="translate(0,-12)">
- <path
- style="display:inline;opacity:1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:212.1320343;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:1.93939996;stroke-opacity:1;paint-order:markers stroke fill"
- d="m 0,17 2,-2 2,-2 2,2 2,2 -2,2 -2,-2 -2,2 z"
- id="rect3900-0"
- inkscape:connector-curvature="0" />
- </g>
-</svg>
diff --git a/src/stylesheets/Paper/Light/Toolbar/archives.svg b/src/stylesheets/Paper/Light/Toolbar/archives.svg
deleted file mode 100644
index cdc3d5a8..00000000
--- a/src/stylesheets/Paper/Light/Toolbar/archives.svg
+++ /dev/null
@@ -1,99 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
- xmlns:dc="http://purl.org/dc/elements/1.1/"
- xmlns:cc="http://creativecommons.org/ns#"
- xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
- xmlns:svg="http://www.w3.org/2000/svg"
- xmlns="http://www.w3.org/2000/svg"
- xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
- xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
- width="128"
- height="128"
- viewBox="0 0 128 128"
- version="1.1"
- id="svg8"
- inkscape:export-filename="D:\Bethesda Game Modding\Mod Organizer 2\stylesheets\Paper\Dark\dots.png"
- inkscape:export-xdpi="307.20001"
- inkscape:export-ydpi="307.20001"
- inkscape:version="0.92.3 (2405546, 2018-03-11)"
- sodipodi:docname="archives.svg">
- <defs
- id="defs2" />
- <sodipodi:namedview
- id="base"
- pagecolor="#505050"
- bordercolor="#666666"
- borderopacity="1.0"
- inkscape:pageopacity="0"
- inkscape:pageshadow="2"
- inkscape:zoom="2.4754625"
- inkscape:cx="153.96887"
- inkscape:cy="-47.187112"
- inkscape:document-units="px"
- inkscape:current-layer="layer25"
- showgrid="false"
- units="px"
- borderlayer="true"
- inkscape:showpageshadow="false"
- inkscape:snap-page="true"
- inkscape:snap-bbox="true"
- inkscape:bbox-nodes="true"
- inkscape:snap-bbox-edge-midpoints="true"
- inkscape:snap-bbox-midpoints="true"
- showguides="true"
- inkscape:guide-bbox="true"
- inkscape:window-width="1920"
- inkscape:window-height="1027"
- inkscape:window-x="-8"
- inkscape:window-y="-8"
- inkscape:window-maximized="1"
- inkscape:snap-center="true"
- inkscape:snap-object-midpoints="true"
- inkscape:measure-start="18.8975,12.498"
- inkscape:measure-end="20,12.498"
- inkscape:snap-intersection-paths="true"
- inkscape:object-paths="true"
- inkscape:snap-smooth-nodes="true"
- inkscape:snap-midpoints="true"
- inkscape:snap-global="true"
- inkscape:lockguides="true">
- <sodipodi:guide
- position="64,64"
- orientation="1,0"
- id="guide4039"
- inkscape:locked="true" />
- <sodipodi:guide
- position="64,64"
- orientation="0,1"
- id="guide3935"
- inkscape:locked="true"
- inkscape:label=""
- inkscape:color="rgb(0,0,255)" />
- </sodipodi:namedview>
- <metadata
- id="metadata5">
- <rdf:RDF>
- <cc:Work
- rdf:about="">
- <dc:format>image/svg+xml</dc:format>
- <dc:type
- rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
- <dc:title />
- </cc:Work>
- </rdf:RDF>
- </metadata>
- <g
- inkscape:groupmode="layer"
- id="layer25"
- inkscape:label="Content"
- transform="translate(0,108)">
- <path
- style="opacity:1;fill:#3d3d3d;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:8;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:normal"
- d="m 73.998642,-83.999999 -9.998644,10.001356 H 14.001355 V -4.000001 H 113.99864 V -73.998643 L 104,-83.999999 H 88.999315 Z"
- id="path3849"
- inkscape:connector-curvature="0"
- sodipodi:nodetypes="ccccccccc" />
- </g>
-</svg>
diff --git a/src/stylesheets/Paper/Light/Toolbar/executables.svg b/src/stylesheets/Paper/Light/Toolbar/executables.svg
deleted file mode 100644
index 64985f45..00000000
--- a/src/stylesheets/Paper/Light/Toolbar/executables.svg
+++ /dev/null
@@ -1,99 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
- xmlns:dc="http://purl.org/dc/elements/1.1/"
- xmlns:cc="http://creativecommons.org/ns#"
- xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
- xmlns:svg="http://www.w3.org/2000/svg"
- xmlns="http://www.w3.org/2000/svg"
- xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
- xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
- width="128"
- height="128"
- viewBox="0 0 128 128"
- version="1.1"
- id="svg8"
- inkscape:export-filename="D:\Bethesda Game Modding\Mod Organizer 2\stylesheets\Paper\Dark\dots.png"
- inkscape:export-xdpi="307.20001"
- inkscape:export-ydpi="307.20001"
- inkscape:version="0.92.3 (2405546, 2018-03-11)"
- sodipodi:docname="executables.svg">
- <defs
- id="defs2" />
- <sodipodi:namedview
- id="base"
- pagecolor="#505050"
- bordercolor="#666666"
- borderopacity="1.0"
- inkscape:pageopacity="0"
- inkscape:pageshadow="2"
- inkscape:zoom="2.0337335"
- inkscape:cx="120.47256"
- inkscape:cy="16.154656"
- inkscape:document-units="px"
- inkscape:current-layer="layer25"
- showgrid="false"
- units="px"
- borderlayer="true"
- inkscape:showpageshadow="false"
- inkscape:snap-page="true"
- inkscape:snap-bbox="true"
- inkscape:bbox-nodes="true"
- inkscape:snap-bbox-edge-midpoints="true"
- inkscape:snap-bbox-midpoints="true"
- showguides="true"
- inkscape:guide-bbox="true"
- inkscape:window-width="1920"
- inkscape:window-height="1027"
- inkscape:window-x="-8"
- inkscape:window-y="-8"
- inkscape:window-maximized="1"
- inkscape:snap-center="true"
- inkscape:snap-object-midpoints="true"
- inkscape:measure-start="18.8975,12.498"
- inkscape:measure-end="20,12.498"
- inkscape:snap-intersection-paths="true"
- inkscape:object-paths="true"
- inkscape:snap-smooth-nodes="true"
- inkscape:snap-midpoints="true"
- inkscape:snap-global="true">
- <sodipodi:guide
- position="64,64"
- orientation="1,0"
- id="guide4039"
- inkscape:locked="false" />
- <sodipodi:guide
- position="64,64"
- orientation="0,1"
- id="guide3935"
- inkscape:locked="false"
- inkscape:label=""
- inkscape:color="rgb(0,0,255)" />
- </sodipodi:namedview>
- <metadata
- id="metadata5">
- <rdf:RDF>
- <cc:Work
- rdf:about="">
- <dc:format>image/svg+xml</dc:format>
- <dc:type
- rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
- <dc:title />
- </cc:Work>
- </rdf:RDF>
- </metadata>
- <g
- inkscape:groupmode="layer"
- id="layer25"
- inkscape:label="Content"
- transform="translate(0,108)">
- <path
- inkscape:connector-curvature="0"
- style="display:inline;opacity:1;fill:#3d3d3d;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:3.00000048;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:stroke fill markers"
- d="m 69.224291,-99 v 25.297161 A 57.019516,45.615413 0 0 0 12.206203,-28.088371 57.019516,45.615413 0 0 0 40.038762,11 h 7.071835 A 34.069164,27.255211 0 0 1 35.161239,-9.7379829 34.069164,27.255211 0 0 1 69.224291,-36.979329 v 25.297161 L 115.7938,-55.341082 Z"
- id="rect3929"
- inkscape:transform-center-x="-6.418094"
- inkscape:transform-center-y="3.2118799" />
- </g>
-</svg>
diff --git a/src/stylesheets/Paper/Light/Toolbar/help.svg b/src/stylesheets/Paper/Light/Toolbar/help.svg
deleted file mode 100644
index 929cd6cd..00000000
--- a/src/stylesheets/Paper/Light/Toolbar/help.svg
+++ /dev/null
@@ -1,128 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
- xmlns:dc="http://purl.org/dc/elements/1.1/"
- xmlns:cc="http://creativecommons.org/ns#"
- xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
- xmlns:svg="http://www.w3.org/2000/svg"
- xmlns="http://www.w3.org/2000/svg"
- xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
- xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
- width="128"
- height="128"
- viewBox="0 0 128 128"
- version="1.1"
- id="svg8"
- inkscape:export-filename="D:\Bethesda Game Modding\Mod Organizer 2\stylesheets\Paper\Dark\dots.png"
- inkscape:export-xdpi="307.20001"
- inkscape:export-ydpi="307.20001"
- inkscape:version="0.92.3 (2405546, 2018-03-11)"
- sodipodi:docname="help.svg">
- <defs
- id="defs2" />
- <sodipodi:namedview
- id="base"
- pagecolor="#505050"
- bordercolor="#666666"
- borderopacity="1.0"
- inkscape:pageopacity="0"
- inkscape:pageshadow="2"
- inkscape:zoom="4.3843014"
- inkscape:cx="50.675733"
- inkscape:cy="33.790362"
- inkscape:document-units="px"
- inkscape:current-layer="layer25"
- showgrid="false"
- units="px"
- borderlayer="true"
- inkscape:showpageshadow="false"
- inkscape:snap-page="true"
- inkscape:snap-bbox="true"
- inkscape:bbox-nodes="true"
- inkscape:snap-bbox-edge-midpoints="true"
- inkscape:snap-bbox-midpoints="true"
- showguides="true"
- inkscape:guide-bbox="true"
- inkscape:window-width="1920"
- inkscape:window-height="1027"
- inkscape:window-x="-8"
- inkscape:window-y="-8"
- inkscape:window-maximized="1"
- inkscape:snap-center="true"
- inkscape:snap-object-midpoints="true"
- inkscape:measure-start="18.8975,12.498"
- inkscape:measure-end="20,12.498"
- inkscape:snap-intersection-paths="true"
- inkscape:object-paths="true"
- inkscape:snap-smooth-nodes="true"
- inkscape:snap-midpoints="true"
- inkscape:snap-global="true">
- <sodipodi:guide
- position="64,64"
- orientation="1,0"
- id="guide4039"
- inkscape:locked="false" />
- <sodipodi:guide
- position="64,64"
- orientation="0,1"
- id="guide3935"
- inkscape:locked="false"
- inkscape:label=""
- inkscape:color="rgb(0,0,255)" />
- </sodipodi:namedview>
- <metadata
- id="metadata5">
- <rdf:RDF>
- <cc:Work
- rdf:about="">
- <dc:format>image/svg+xml</dc:format>
- <dc:type
- rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
- <dc:title />
- </cc:Work>
- </rdf:RDF>
- </metadata>
- <g
- inkscape:groupmode="layer"
- id="layer25"
- inkscape:label="Content"
- transform="translate(0,108)">
- <circle
- style="opacity:1;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:#3d3d3d;stroke-width:8;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:stroke fill markers"
- id="path3848"
- cx="64"
- cy="-44"
- r="60" />
- <g
- id="g3871"
- transform="translate(0,-1.242582)"
- style="stroke-width:1;fill:#3d3d3d;fill-opacity:1">
- <path
- id="path3851"
- d="m 64,-80 c -4.113298,1.67e-4 -7.447744,3.334612 -7.44791,7.44791 -6.7e-4,4.11389 3.33402,7.449258 7.44791,7.449426 4.11389,-1.68e-4 7.44858,-3.335536 7.44791,-7.449426 C 71.447743,-76.665387 68.113298,-79.999833 64,-80 Z"
- style="opacity:1;fill:#3d3d3d;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:8;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:stroke fill markers"
- inkscape:connector-curvature="0" />
- <path
- inkscape:connector-curvature="0"
- id="path3860"
- d="m 56.55209,-50.206086 v 34.757418 h 14.89582 v -34.757418 z"
- style="opacity:1;fill:#3d3d3d;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:8;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:stroke fill markers" />
- <path
- style="opacity:1;fill:#3d3d3d;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:8;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:stroke fill markers"
- d="m 64,-80 c -4.113298,1.67e-4 -7.447744,3.334612 -7.44791,7.44791 -6.7e-4,4.11389 3.33402,7.449258 7.44791,7.449426 4.11389,-1.68e-4 7.44858,-3.335536 7.44791,-7.449426 C 71.447743,-76.665387 68.113298,-79.999833 64,-80 Z"
- id="path3853"
- inkscape:connector-curvature="0" />
- <path
- inkscape:connector-curvature="0"
- id="path3862"
- d="m 64,-57.654754 c -4.113298,1.67e-4 -7.447744,3.334612 -7.44791,7.44791 -6.7e-4,4.11389 3.33402,7.449258 7.44791,7.449426 4.11389,-1.68e-4 7.44858,-3.335536 7.44791,-7.449426 -1.67e-4,-4.113297 -3.334612,-7.447743 -7.44791,-7.44791 z"
- style="opacity:1;fill:#3d3d3d;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:8;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:stroke fill markers" />
- <path
- style="opacity:1;fill:#3d3d3d;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:8;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:stroke fill markers"
- d="m 64,-22.897336 c -4.113298,1.67e-4 -7.447744,3.334612 -7.44791,7.44791 C 56.55142,-11.335536 59.88611,-8.000168 64,-8 c 4.11389,-1.68e-4 7.44858,-3.335536 7.44791,-7.449426 -1.67e-4,-4.113297 -3.334612,-7.447743 -7.44791,-7.44791 z"
- id="path3864"
- inkscape:connector-curvature="0" />
- </g>
- </g>
-</svg>
diff --git a/src/stylesheets/Paper/Light/Toolbar/instances.svg b/src/stylesheets/Paper/Light/Toolbar/instances.svg
deleted file mode 100644
index 78361fe9..00000000
--- a/src/stylesheets/Paper/Light/Toolbar/instances.svg
+++ /dev/null
@@ -1,98 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
- xmlns:dc="http://purl.org/dc/elements/1.1/"
- xmlns:cc="http://creativecommons.org/ns#"
- xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
- xmlns:svg="http://www.w3.org/2000/svg"
- xmlns="http://www.w3.org/2000/svg"
- xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
- xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
- width="128"
- height="128"
- viewBox="0 0 128 128"
- version="1.1"
- id="svg8"
- inkscape:export-filename="D:\Bethesda Game Modding\Mod Organizer 2\stylesheets\Paper\Dark\dots.png"
- inkscape:export-xdpi="307.20001"
- inkscape:export-ydpi="307.20001"
- inkscape:version="0.92.3 (2405546, 2018-03-11)"
- sodipodi:docname="instances.svg">
- <defs
- id="defs2" />
- <sodipodi:namedview
- id="base"
- pagecolor="#505050"
- bordercolor="#666666"
- borderopacity="1.0"
- inkscape:pageopacity="0"
- inkscape:pageshadow="2"
- inkscape:zoom="4.9509251"
- inkscape:cx="95.667898"
- inkscape:cy="15.212052"
- inkscape:document-units="px"
- inkscape:current-layer="layer25"
- showgrid="false"
- units="px"
- borderlayer="true"
- inkscape:showpageshadow="false"
- inkscape:snap-page="true"
- inkscape:snap-bbox="true"
- inkscape:bbox-nodes="true"
- inkscape:snap-bbox-edge-midpoints="true"
- inkscape:snap-bbox-midpoints="true"
- showguides="true"
- inkscape:guide-bbox="true"
- inkscape:window-width="1920"
- inkscape:window-height="1027"
- inkscape:window-x="-8"
- inkscape:window-y="-8"
- inkscape:window-maximized="1"
- inkscape:snap-center="true"
- inkscape:snap-object-midpoints="true"
- inkscape:measure-start="18.8975,12.498"
- inkscape:measure-end="20,12.498"
- inkscape:snap-intersection-paths="true"
- inkscape:object-paths="true"
- inkscape:snap-smooth-nodes="true"
- inkscape:snap-midpoints="true"
- inkscape:snap-global="true">
- <sodipodi:guide
- position="64,64"
- orientation="1,0"
- id="guide4039"
- inkscape:locked="false" />
- <sodipodi:guide
- position="64,64"
- orientation="0,1"
- id="guide3935"
- inkscape:locked="false"
- inkscape:label=""
- inkscape:color="rgb(0,0,255)" />
- </sodipodi:namedview>
- <metadata
- id="metadata5">
- <rdf:RDF>
- <cc:Work
- rdf:about="">
- <dc:format>image/svg+xml</dc:format>
- <dc:type
- rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
- <dc:title />
- </cc:Work>
- </rdf:RDF>
- </metadata>
- <g
- inkscape:groupmode="layer"
- id="layer25"
- inkscape:label="Content"
- transform="translate(0,108)">
- <path
- style="opacity:1;fill:#3d3d3d;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:8;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:normal"
- d="m 43.074486,-84 c -7.191305,0.179697 -13.681107,2.360737 -19.604836,6.184844 l -0.829325,3.561678 -4.167425,3.208112 -5.581697,14.982451 c -5.7982022,15.544107 -18.3828084,44.900541 4.037437,52.0629145 5.467067,-4.389774 11.744564,-10.4769935 18.039776,-16.6021055 3.729503,-3.62877 7.012953,-4.081632 14.160925,-4.081632 h 14.865463 14.865462 c 7.147976,0 10.431426,0.452872 14.160926,4.081632 6.2952,6.125112 12.583088,12.2123315 18.050168,16.6021055 22.42024,-7.1623735 9.83564,-36.5188075 4.03744,-52.0629145 l -5.5921,-14.982451 -4.15702,-3.208112 -0.82933,-3.561678 C 98.606622,-81.639263 92.106422,-83.820123 84.915122,-84 l -5.46211,2.979332 H 63.994804 48.546994 Z m 51.652146,11.766541 c 2.28943,-7.75e-4 4.14597,1.854604 4.14663,4.144028 7.8e-4,2.290441 -1.85618,4.147401 -4.14663,4.146627 -2.28942,-6.62e-4 -4.1448,-1.857203 -4.14402,-4.146627 6.6e-4,-2.288409 1.85562,-4.143366 4.14402,-4.144028 z m -61.453263,4.144028 c 4.578848,-1.12e-4 8.290765,3.711805 8.290653,8.290653 1.13e-4,4.578848 -3.711805,8.290766 -8.290653,8.290655 -4.578848,1.13e-4 -8.290768,-3.711807 -8.290654,-8.290655 -1.12e-4,-4.578848 3.711806,-8.290766 8.290654,-8.290653 z m 53.162613,4.146627 c 2.28943,-7.75e-4 4.14597,1.854603 4.14663,4.144026 7.7e-4,2.290441 -1.85619,4.147403 -4.14663,4.146627 -2.289423,-6.62e-4 -4.144799,-1.857203 -4.144025,-4.146627 6.62e-4,-2.288409 1.855618,-4.143365 4.144025,-4.144026 z m 16.581308,0 c 2.28942,-7.75e-4 4.14596,1.854603 4.14663,4.144026 7.7e-4,2.290441 -1.85619,4.147403 -4.14663,4.146627 -2.28943,-6.62e-4 -4.144798,-1.857203 -4.144028,-4.146627 6.6e-4,-2.288409 1.855618,-4.143365 4.144028,-4.144026 z m -8.290658,8.290654 c 2.28943,-7.75e-4 4.14597,1.854602 4.14663,4.144027 7.8e-4,2.29044 -1.85618,4.147401 -4.14663,4.146626 -2.28942,-6.62e-4 -4.1448,-1.857203 -4.14402,-4.146626 6.6e-4,-2.28841 1.85562,-4.143366 4.14402,-4.144027 z m -48.662414,6.184843 h 5.527101 v 5.527103 h 5.527103 v 5.527102 H 51.591319 V -32.886 h -5.527101 v -5.527102 h -5.527103 v -5.527102 h 5.527103 z m 33.10802,0 c 4.578848,-1.12e-4 8.290764,3.711806 8.290654,8.290654 1.1e-4,4.578848 -3.711806,8.290765 -8.290654,8.290653 -4.578848,1.13e-4 -8.290766,-3.711805 -8.290654,-8.290653 -1.13e-4,-4.578848 3.711806,-8.290767 8.290654,-8.290654 z"
- id="path3849"
- inkscape:connector-curvature="0"
- sodipodi:nodetypes="cccccccscscccccccccccccccccccccccccccccccccccccccccccccccccccccc" />
- </g>
-</svg>
diff --git a/src/stylesheets/Paper/Light/Toolbar/nexus.svg b/src/stylesheets/Paper/Light/Toolbar/nexus.svg
deleted file mode 100644
index 1715b626..00000000
--- a/src/stylesheets/Paper/Light/Toolbar/nexus.svg
+++ /dev/null
@@ -1,122 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
- xmlns:dc="http://purl.org/dc/elements/1.1/"
- xmlns:cc="http://creativecommons.org/ns#"
- xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
- xmlns:svg="http://www.w3.org/2000/svg"
- xmlns="http://www.w3.org/2000/svg"
- xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
- xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
- width="128"
- height="128"
- viewBox="0 0 128 128"
- version="1.1"
- id="svg8"
- inkscape:export-filename="D:\Bethesda Game Modding\Mod Organizer 2\stylesheets\Paper\Dark\dots.png"
- inkscape:export-xdpi="307.20001"
- inkscape:export-ydpi="307.20001"
- inkscape:version="0.92.3 (2405546, 2018-03-11)"
- sodipodi:docname="nexus.svg">
- <defs
- id="defs2" />
- <sodipodi:namedview
- id="base"
- pagecolor="#505050"
- bordercolor="#666666"
- borderopacity="1.0"
- inkscape:pageopacity="0"
- inkscape:pageshadow="2"
- inkscape:zoom="4.2612171"
- inkscape:cx="56.087807"
- inkscape:cy="27.420714"
- inkscape:document-units="px"
- inkscape:current-layer="layer25"
- showgrid="false"
- units="px"
- borderlayer="true"
- inkscape:showpageshadow="false"
- inkscape:snap-page="true"
- inkscape:snap-bbox="true"
- inkscape:bbox-nodes="true"
- inkscape:snap-bbox-edge-midpoints="true"
- inkscape:snap-bbox-midpoints="true"
- showguides="true"
- inkscape:guide-bbox="true"
- inkscape:window-width="1920"
- inkscape:window-height="1027"
- inkscape:window-x="-8"
- inkscape:window-y="-8"
- inkscape:window-maximized="1"
- inkscape:snap-center="true"
- inkscape:snap-object-midpoints="true"
- inkscape:measure-start="108.073,108.073"
- inkscape:measure-end="112.737,112.737"
- inkscape:snap-intersection-paths="true"
- inkscape:object-paths="true"
- inkscape:snap-smooth-nodes="true"
- inkscape:snap-midpoints="true"
- inkscape:snap-global="true">
- <sodipodi:guide
- position="64,64"
- orientation="1,0"
- id="guide4039"
- inkscape:locked="false" />
- <sodipodi:guide
- position="64,64"
- orientation="0,1"
- id="guide3935"
- inkscape:locked="false"
- inkscape:label="255"
- inkscape:color="rgb(0,0,255)" />
- </sodipodi:namedview>
- <metadata
- id="metadata5">
- <rdf:RDF>
- <cc:Work
- rdf:about="">
- <dc:format>image/svg+xml</dc:format>
- <dc:type
- rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
- <dc:title />
- </cc:Work>
- </rdf:RDF>
- </metadata>
- <g
- inkscape:groupmode="layer"
- id="layer25"
- inkscape:label="Content"
- transform="translate(0,108)">
- <path
- style="opacity:1;fill:#3d3d3d;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:7.99999952;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:normal"
- d="m 37.578906,-103.99986 -2.327345,1.09219 c -5.625077,2.65309 -12.341703,6.959572 -18.124217,14.742184 l -1.439062,1.942967 -0.3,2.397657 c -0.727944,5.785703 0.645537,10.927203 2.601562,15.550782 -3.97001,7.523426 -6.539675,15.713366 -6.546095,24.274216 h 0.01176 v 0.06094 c 0.01254,2.097981 0.399663,4.072256 0.646875,6.100783 -3.5620623,4.427533 -7.0313403,9.956846 -7.8375002,17.704685 l -0.2648844,2.554689 1.0921859,2.327343 c 2.6531016,5.6250757 6.9595777,12.341692 14.7421887,18.12422 l 1.94297,1.4390639 2.397656,0.3 c 5.785701,0.727944 10.927203,-0.6455399 15.55078,-2.6015644 7.523428,3.9700084 15.713368,6.539664 24.274217,6.546096 v -0.012 h 0.06094 c 2.097982,-0.0126 4.072257,-0.39966 6.100783,-0.646872 4.427536,3.5620555 9.956847,7.0313275 17.704687,7.8374995 l 2.554687,0.26484 2.327343,-1.092192 C 98.373512,12.254575 105.09014,7.9480995 110.87265,0.1654875 l 1.43672,-1.9429715 0.30236,-2.3976556 c 0.72794,-5.7854267 -0.64555,-10.9269284 -2.60157,-15.5505044 3.97001,-7.523428 6.53967,-15.713368 6.5461,-24.27422 h -0.012 v -0.06094 c -0.0133,-2.097984 -0.39967,-4.072258 -0.64688,-6.100783 3.56206,-4.427535 7.03131,-9.956848 7.8375,-17.704688 L 124,-70.420959 l -1.09219,-2.327345 c -2.6531,-5.625076 -6.95957,-12.341702 -14.74219,-18.124218 l -1.94297,-1.436719 -2.39765,-0.302345 c -5.785706,-0.727943 -10.927203,0.64554 -15.550778,2.601564 -7.523434,-3.970005 -15.713374,-6.539667 -24.274225,-6.546088 v 0.01172 h -0.06094 c -2.097982,0.01266 -4.072255,0.399662 -6.100782,0.646874 -4.427534,-3.562058 -9.956847,-7.031344 -17.704687,-7.837494 z m 1.485937,6.351563 c 0.03984,-7.78e-4 0.07969,-7.78e-4 0.119532,0 0.08844,8.72e-4 0.176806,0.0056 0.264842,0.01406 0.01563,-1.23e-4 0.03128,-1.23e-4 0.04686,0 0.06038,0.0056 0.120558,0.01378 0.180468,0.02344 0.03369,0.0037 0.06016,0.01978 0.09376,0.02344 8.119439,0.924618 13.438311,4.913385 17.695312,8.847652 0.518092,0.475264 0.857314,1.113992 0.960938,1.809374 1.846847,-0.259602 3.708531,-0.399736 5.573432,-0.419528 7.414774,0.0056 14.704137,1.913317 21.171093,5.540624 3.592503,-2.496611 7.615515,-4.335787 12.384376,-4.79531 1.744997,-0.168149 3.586634,-0.159114 5.503124,0.082 0.5255,0.06779 1.0245,0.270451 1.44845,0.588283 6.86031,5.097232 10.33844,10.548846 12.76641,15.658591 0.0245,0.05156 0.0622,0.100836 0.0867,0.152346 0.0157,0.04031 0.0306,0.08093 0.0445,0.121879 0.0412,0.09035 0.0779,0.182649 0.11015,0.276564 0,0.0029 0,0.0063 0,0.0094 4e-5,0.0086 4e-5,0.01718 0,0.02577 0.0284,0.09024 0.0527,0.18174 0.0727,0.274217 0.0197,0.09143 0.0355,0.18371 0.0469,0.276565 1e-5,0.0079 1e-5,0.01562 0,0.02344 0.005,0.06 0.0108,0.120187 0.0141,0.180468 7.7e-4,0.03984 7.7e-4,0.07969 0,0.119532 -8.7e-4,0.08844 -0.005,0.176808 -0.0141,0.264844 1.2e-4,0.01563 1.2e-4,0.03125 0,0.04685 -0.008,0.06038 -0.0139,0.12056 -0.0233,0.180468 -0.004,0.03369 -0.0199,0.06016 -0.0234,0.09376 -0.92463,8.119438 -4.91339,13.438312 -8.84766,17.695313 -0.47526,0.51809 -1.114,0.857312 -1.80939,0.960935 0.25962,1.846853 0.39976,3.708535 0.41955,5.573439 -0.005,7.414773 -1.91333,14.704135 -5.54064,21.171093 2.49662,3.592503 4.33578,7.615515 4.79533,12.384375 0.16814,1.7449932 0.1591,3.586632 -0.0819,5.5031256 -0.0677,0.5254956 -0.27045,1.0244952 -0.58828,1.4484372 -5.09716,6.8603052 -10.548768,10.3384447 -15.658515,12.7664047 -0.05156,0.02448 -0.100836,0.06228 -0.152342,0.08676 -0.04031,0.01572 -0.08092,0.0306 -0.12188,0.04452 -0.09035,0.04116 -0.182648,0.07788 -0.276562,0.11016 -0.0031,0 -0.0063,0 -0.0094,0 -0.0086,3.5e-5 -0.01718,3.5e-5 -0.02577,0 -0.09024,0.02844 -0.181741,0.05268 -0.274216,0.07272 -0.09143,0.0198 -0.183711,0.03552 -0.276563,0.04692 -0.0079,2.4e-5 -0.01563,2.4e-5 -0.02344,0 -0.06,0.006 -0.120186,0.0108 -0.180468,0.01404 -0.03984,7.8e-4 -0.0797,7.8e-4 -0.119532,0 -0.08844,-8.64e-4 -0.176806,-0.006 -0.264846,-0.01404 -0.01563,1.2e-4 -0.03128,1.2e-4 -0.04686,0 -0.06037,-0.006 -0.120559,-0.0138 -0.180469,-0.0234 -0.03369,-0.0036 -0.06014,-0.0198 -0.09376,-0.0234 C 80.110654,8.6632245 74.79178,4.6744608 70.534779,0.740188 c -0.51809,-0.47526 -0.857313,-1.113984 -0.960936,-1.8093745 -1.847084,0.259404 -3.708765,0.3995425 -5.573668,0.4193305 -7.414776,-0.006 -14.704135,-1.9133172 -21.171094,-5.5406236 -3.592503,2.4966096 -7.615515,4.3357863 -12.384377,4.7953119 -1.744993,0.1681465 -3.586632,0.1591105 -5.503122,-0.082008 -0.525498,-0.067788 -1.024497,-0.2704523 -1.448438,-0.5882844 -6.860299,-5.0972295 -10.338443,-10.5488439 -12.766408,-15.6585929 -0.0245,-0.05156 -0.06234,-0.10086 -0.08672,-0.152343 -0.0157,-0.0403 -0.03056,-0.08095 -0.04453,-0.12188 -0.04117,-0.09035 -0.07795,-0.182646 -0.11016,-0.276563 -6e-6,-0.0031 -6e-6,-0.0063 0,-0.0094 -3.6e-5,-0.0086 -3.6e-5,-0.01718 0,-0.02577 -0.02843,-0.09024 -0.05268,-0.181742 -0.07265,-0.274218 -0.01984,-0.09143 -0.03547,-0.183709 -0.04685,-0.276562 -3.1e-5,-0.0077 -3.1e-5,-0.01562 0,-0.02344 -0.0067,-0.06 -0.01116,-0.120186 -0.0141,-0.180469 -7.76e-4,-0.03984 -7.76e-4,-0.07969 0,-0.119532 8.7e-4,-0.08844 0.0053,-0.176808 0.0141,-0.264847 -1.19e-4,-0.01563 -1.19e-4,-0.03125 0,-0.04685 0.006,-0.06037 0.0138,-0.120559 0.02344,-0.180469 0.0037,-0.03369 0.01981,-0.06014 0.02344,-0.09376 0.924627,-8.119438 4.91339,-13.438313 8.847658,-17.695312 0.475263,-0.518093 1.113993,-0.857315 1.809373,-0.960938 -0.259589,-1.846868 -0.399724,-3.708551 -0.419516,-5.573451 0.0056,-7.414775 1.913317,-14.704137 5.540624,-21.171094 -2.49661,-3.592502 -4.335787,-7.615515 -4.79531,-12.384377 -0.168149,-1.744994 -0.159113,-3.58663 0.082,-5.503124 0.06779,-0.525498 0.270452,-1.024496 0.588283,-1.448437 5.097232,-6.860299 10.548845,-10.338443 15.658591,-12.766403 0.05156,-0.02447 0.100837,-0.06234 0.152347,-0.08672 0.04031,-0.01575 0.08093,-0.03056 0.121878,-0.04453 0.09035,-0.04116 0.182646,-0.07791 0.276563,-0.110157 0.0029,0 0.0063,0 0.0094,0 0.0086,-3.7e-5 0.01718,-3.7e-5 0.02578,0 0.09024,-0.02841 0.181741,-0.05269 0.274218,-0.07266 0.09143,-0.01987 0.183711,-0.03544 0.276563,-0.04687 0.0077,-2.8e-5 0.01563,-2.8e-5 0.02344,0 0.06,-0.0066 0.120186,-0.01116 0.180468,-0.01416 z m 0.119532,3.070311 c -4.918893,2.320027 -9.830756,5.400922 -14.6625,11.903905 -0.106116,0.843338 -0.161184,1.665537 -0.171096,2.467968 -0.0098,0.802433 0.02693,1.584662 0.100776,2.350782 1.033521,10.725648 10.216452,17.923996 18.292968,25.258593 l -3.764063,4.246876 21.255469,-0.131254 -2.578125,-20.934377 -4.446092,5.025 c -7.369823,-8.23868 -18.074776,-20.906037 -14.02734,-30.187495 z m 3.009373,0.623437 c -0.04253,0.205977 -0.106176,0.407033 -0.189841,0.599999 -0.355764,0.815838 -0.552587,1.702983 -0.602346,2.660157 -0.01586,0.305199 -0.01586,0.61855 0,0.9375 0.111036,2.232626 0.98277,4.789133 2.3625,7.502343 3.563011,-1.890229 7.372419,-3.27389 11.317968,-4.110937 l 0.3,-0.119533 c -3.54753,-3.278554 -7.503737,-6.172443 -13.188279,-7.46953 z m 12.2625,4.582031 -0.215624,0.07031 -0.01176,0.02344 c 0.08105,-0.01726 0.146146,-0.07651 0.227344,-0.09376 z m 45.750002,5.723437 c -0.802434,-0.0098 -1.584666,0.02696 -2.350781,0.100775 -10.725652,1.03352 -17.923998,10.216453 -25.258595,18.292968 l -4.246878,-3.764057 0.131254,21.255469 20.934376,-2.578123 -5.025,-4.446096 c 8.238683,-7.369823 20.906044,-18.072433 30.187494,-14.025 -2.32001,-4.918892 -5.40091,-9.833098 -11.9039,-14.664842 -0.84333,-0.106117 -1.66554,-0.161182 -2.46797,-0.171094 z m 9.55079,17.050783 c -2.23265,0.111035 -4.78915,0.982766 -7.50235,2.362499 1.89023,3.563012 3.27389,7.372418 4.11093,11.317966 l 0.11965,0.3 c 3.27856,-3.547531 6.17244,-7.503737 7.46954,-13.18828 -0.20598,-0.04252 -0.40703,-0.106175 -0.6,-0.189842 -0.81584,-0.355764 -1.70298,-0.552585 -2.66016,-0.602343 -0.30519,-0.01586 -0.61855,-0.01586 -0.9375,0 z m -0.47813,12.827343 c 0.0173,0.08105 0.0764,0.146144 0.0937,0.227342 l -0.0702,-0.215625 z m -49.762503,13.535156 -20.932032,2.578124 5.025,4.446095 c -8.238684,7.369824 -20.906042,18.074776 -30.1875,14.027343 2.320026,4.918894 5.400921,9.8307519 11.903905,14.662499 0.843337,0.106116 1.665539,0.1611829 2.467968,0.1710949 0.802431,0.00984 1.584663,-0.026952 2.350782,-0.100776 10.72565,-1.0335217 17.923997,-10.2164539 25.258593,-18.2929699 l 4.246874,3.764056 z m 29.503121,0.585935 -21.255467,0.133592 2.578124,20.932032 4.446095,-5.025 c 7.369824,8.238685 18.072432,20.9060398 14.024999,30.1875015 4.91889,-2.3200323 9.833102,-5.4009245 14.664841,-11.9039095 0.10608,-0.8433371 0.16119,-1.6655363 0.1711,-2.4679691 0.01,-0.8024304 -0.027,-1.5846612 -0.1008,-2.3507799 C 102.5149,-20.870044 93.331956,-28.06839 85.255441,-35.402988 Z m -67.504684,4.267971 c -3.278554,3.547531 -6.172444,7.503737 -7.469533,13.18828 0.205978,0.04256 0.407034,0.106176 0.6,0.189843 0.815838,0.355762 1.702981,0.552586 2.660158,0.602343 0.305198,0.01586 0.618549,0.01586 0.9375,0 2.232626,-0.111036 4.789133,-0.982768 7.502342,-2.3625 -1.890227,-3.56301 -3.273887,-7.372418 -4.110936,-11.317966 z m -2.8875,0.92578 0.0703,0.215625 0.02344,0.01176 c -0.01726,-0.08105 -0.0765,-0.146143 -0.09375,-0.227344 z m 65.608592,28.7109367 c -3.563012,1.8902291 -7.37242,3.2738893 -11.317967,4.1109373 l -0.3,0.119532 c 3.54753,3.2785476 7.503737,6.1724429 13.188278,7.4695349 0.04252,-0.20598 0.106177,-0.407028 0.189844,-0.6 0.355763,-0.815844 0.552585,-1.7029799 0.602345,-2.6601593 0.01586,-0.305196 0.01586,-0.618552 0,-0.9375 -0.111036,-2.2326384 -0.982768,-4.789134 -2.3625,-7.5023449 z M 73.771091,1.2790394 c -0.08105,0.01728 -0.146145,0.07644 -0.227344,0.09372 l 0.215627,-0.07032 z"
- id="path3849"
- inkscape:connector-curvature="0"
- sodipodi:nodetypes="cccccccccccccccccccccccccccccccccccccccccccccccccccccccscccccccccccccccccccccscccccccccccccccccccccscccccccccccccccccccccscccccccccccccccccccccccccsccccccccccccccccccccccccccccccccccccccccccccccccccccccsccccccccccccccccccc" />
- <g
- style="fill:#505050;fill-opacity:1;stroke-width:0.19550784"
- id="g4412"
- inkscape:transform-center-x="-5.0913502"
- inkscape:transform-center-y="-4.4596393"
- transform="matrix(5.1148849,0,0,5.1148849,12.851148,-95.148848)" />
- <g
- transform="matrix(0,4.9102895,-4.9102895,0,113.1029,-93.102895)"
- inkscape:transform-center-y="24"
- inkscape:transform-center-x="-21.022466"
- id="g4064"
- style="fill:#505050;fill-opacity:1;stroke-width:0.20365398" />
- <g
- style="fill:#505050;fill-opacity:1;stroke-width:0.20365398"
- id="g4068"
- inkscape:transform-center-x="24.000005"
- inkscape:transform-center-y="21.022466"
- transform="matrix(-4.9102895,0,0,-4.9102895,113.10289,5.1029002)" />
- <g
- transform="matrix(-2.719147e-8,-4.9102894,4.9102894,-2.719147e-8,14.8971,5.1028901)"
- inkscape:transform-center-y="-24.000003"
- inkscape:transform-center-x="21.022466"
- id="g4072"
- style="fill:#505050;fill-opacity:1;stroke-width:0.20365398" />
- </g>
-</svg>
diff --git a/src/stylesheets/Paper/Light/Toolbar/problems.svg b/src/stylesheets/Paper/Light/Toolbar/problems.svg
deleted file mode 100644
index 9d398ee2..00000000
--- a/src/stylesheets/Paper/Light/Toolbar/problems.svg
+++ /dev/null
@@ -1,132 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
- xmlns:dc="http://purl.org/dc/elements/1.1/"
- xmlns:cc="http://creativecommons.org/ns#"
- xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
- xmlns:svg="http://www.w3.org/2000/svg"
- xmlns="http://www.w3.org/2000/svg"
- xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
- xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
- width="128"
- height="128"
- viewBox="0 0 128 128"
- version="1.1"
- id="svg8"
- inkscape:export-filename="D:\Bethesda Game Modding\Mod Organizer 2\stylesheets\Paper\Dark\dots.png"
- inkscape:export-xdpi="307.20001"
- inkscape:export-ydpi="307.20001"
- inkscape:version="0.92.3 (2405546, 2018-03-11)"
- sodipodi:docname="problems.svg">
- <defs
- id="defs2" />
- <sodipodi:namedview
- id="base"
- pagecolor="#505050"
- bordercolor="#666666"
- borderopacity="1.0"
- inkscape:pageopacity="0"
- inkscape:pageshadow="2"
- inkscape:zoom="6.6922248"
- inkscape:cx="59.799827"
- inkscape:cy="56.347509"
- inkscape:document-units="px"
- inkscape:current-layer="layer25"
- showgrid="false"
- units="px"
- borderlayer="true"
- inkscape:showpageshadow="false"
- inkscape:snap-page="true"
- inkscape:snap-bbox="true"
- inkscape:bbox-nodes="true"
- inkscape:snap-bbox-edge-midpoints="true"
- inkscape:snap-bbox-midpoints="true"
- showguides="false"
- inkscape:guide-bbox="true"
- inkscape:window-width="1920"
- inkscape:window-height="1027"
- inkscape:window-x="-8"
- inkscape:window-y="-8"
- inkscape:window-maximized="1"
- inkscape:snap-center="true"
- inkscape:snap-object-midpoints="true"
- inkscape:measure-start="18.8975,12.498"
- inkscape:measure-end="20,12.498"
- inkscape:snap-intersection-paths="true"
- inkscape:object-paths="true"
- inkscape:snap-smooth-nodes="true"
- inkscape:snap-midpoints="true"
- inkscape:snap-global="true">
- <sodipodi:guide
- position="64,64"
- orientation="1,0"
- id="guide4039"
- inkscape:locked="false" />
- <sodipodi:guide
- position="64,64"
- orientation="0,1"
- id="guide3935"
- inkscape:locked="false"
- inkscape:label=""
- inkscape:color="rgb(0,0,255)" />
- </sodipodi:namedview>
- <metadata
- id="metadata5">
- <rdf:RDF>
- <cc:Work
- rdf:about="">
- <dc:format>image/svg+xml</dc:format>
- <dc:type
- rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
- <dc:title />
- </cc:Work>
- </rdf:RDF>
- </metadata>
- <g
- inkscape:groupmode="layer"
- id="layer25"
- inkscape:label="Content"
- transform="translate(0,108)">
- <g
- id="g3861"
- transform="matrix(0.55290621,0,0,0.55290621,28.614002,-29.750801)"
- style="fill:#737373;fill-opacity:1;stroke-width:1.80862486" />
- <path
- style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:#3d3d3d;stroke-width:7.99999952;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
- d="m 64.344931,-96.263216 c -2.808173,-0.131523 -5.455193,1.310482 -6.861125,3.737706 L 4.9422487,-1.7926865 C 2.0494853,3.2027814 5.6651359,9.4471972 11.450616,9.4476002 H 116.53373 c 5.78548,-4.1e-4 9.40112,-6.2448225 6.50836,-11.2402867 L 70.500541,-92.52551 c -1.279824,-2.209515 -3.599319,-3.61792 -6.15561,-3.737706 z"
- id="path3875"
- inkscape:connector-curvature="0"
- sodipodi:nodetypes="cccccccc" />
- <g
- transform="matrix(-0.83333333,0,0,-0.83333333,117.3255,-70.7681)"
- id="g3871"
- style="stroke-width:1.20000005;fill:#3d3d3d;fill-opacity:1">
- <path
- inkscape:connector-curvature="0"
- id="path3851"
- d="m 64,-80 c -4.113298,1.67e-4 -7.447744,3.334612 -7.44791,7.44791 -6.7e-4,4.11389 3.33402,7.449258 7.44791,7.449426 4.11389,-1.68e-4 7.44858,-3.335536 7.44791,-7.449426 C 71.447743,-76.665387 68.113298,-79.999833 64,-80 Z"
- style="opacity:1;fill:#3d3d3d;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:9.60000038;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:stroke fill markers" />
- <path
- inkscape:connector-curvature="0"
- id="path3860-8"
- d="m 56.55209,-50.206086 v 34.757418 h 14.89582 v -34.757418 z"
- style="opacity:1;fill:#3d3d3d;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:9.60000038;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:stroke fill markers" />
- <path
- inkscape:connector-curvature="0"
- style="opacity:1;fill:#3d3d3d;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:9.60000038;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:stroke fill markers"
- d="m 64,-80 c -4.113298,1.67e-4 -7.447744,3.334612 -7.44791,7.44791 -6.7e-4,4.11389 3.33402,7.449258 7.44791,7.449426 4.11389,-1.68e-4 7.44858,-3.335536 7.44791,-7.449426 C 71.447743,-76.665387 68.113298,-79.999833 64,-80 Z"
- id="path3853" />
- <path
- inkscape:connector-curvature="0"
- id="path3862"
- d="m 64,-57.654754 c -4.113298,1.67e-4 -7.447744,3.334612 -7.44791,7.44791 -6.7e-4,4.11389 3.33402,7.449258 7.44791,7.449426 4.11389,-1.68e-4 7.44858,-3.335536 7.44791,-7.449426 -1.67e-4,-4.113297 -3.334612,-7.447743 -7.44791,-7.44791 z"
- style="opacity:1;fill:#3d3d3d;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:9.60000038;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:stroke fill markers" />
- <path
- style="opacity:1;fill:#3d3d3d;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:9.60000038;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:stroke fill markers"
- d="m 64,-22.897336 c -4.113298,1.67e-4 -7.447744,3.334612 -7.44791,7.44791 C 56.55142,-11.335536 59.88611,-8.000168 64,-8 c 4.11389,-1.68e-4 7.44858,-3.335536 7.44791,-7.449426 -1.67e-4,-4.113297 -3.334612,-7.447743 -7.44791,-7.44791 z"
- id="path3864"
- inkscape:connector-curvature="0" />
- </g>
- </g>
-</svg>
diff --git a/src/stylesheets/Paper/Light/Toolbar/profiles.svg b/src/stylesheets/Paper/Light/Toolbar/profiles.svg
deleted file mode 100644
index 5c074237..00000000
--- a/src/stylesheets/Paper/Light/Toolbar/profiles.svg
+++ /dev/null
@@ -1,112 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
- xmlns:dc="http://purl.org/dc/elements/1.1/"
- xmlns:cc="http://creativecommons.org/ns#"
- xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
- xmlns:svg="http://www.w3.org/2000/svg"
- xmlns="http://www.w3.org/2000/svg"
- xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
- xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
- width="128"
- height="128"
- viewBox="0 0 128 128"
- version="1.1"
- id="svg8"
- inkscape:export-filename="D:\Bethesda Game Modding\Mod Organizer 2\stylesheets\Paper\Dark\dots.png"
- inkscape:export-xdpi="307.20001"
- inkscape:export-ydpi="307.20001"
- inkscape:version="0.92.3 (2405546, 2018-03-11)"
- sodipodi:docname="profiles.svg">
- <defs
- id="defs2" />
- <sodipodi:namedview
- id="base"
- pagecolor="#505050"
- bordercolor="#666666"
- borderopacity="1.0"
- inkscape:pageopacity="0"
- inkscape:pageshadow="2"
- inkscape:zoom="4.5931435"
- inkscape:cx="69.80099"
- inkscape:cy="16.758863"
- inkscape:document-units="px"
- inkscape:current-layer="layer25"
- showgrid="false"
- units="px"
- borderlayer="true"
- inkscape:showpageshadow="false"
- inkscape:snap-page="true"
- inkscape:snap-bbox="true"
- inkscape:bbox-nodes="true"
- inkscape:snap-bbox-edge-midpoints="true"
- inkscape:snap-bbox-midpoints="true"
- showguides="true"
- inkscape:guide-bbox="true"
- inkscape:window-width="1920"
- inkscape:window-height="1027"
- inkscape:window-x="-8"
- inkscape:window-y="-8"
- inkscape:window-maximized="1"
- inkscape:snap-center="true"
- inkscape:snap-object-midpoints="true"
- inkscape:measure-start="64,11.9981"
- inkscape:measure-end="64,7.93514"
- inkscape:snap-intersection-paths="true"
- inkscape:object-paths="true"
- inkscape:snap-smooth-nodes="true"
- inkscape:snap-midpoints="true"
- inkscape:snap-global="true"
- inkscape:lockguides="true"
- inkscape:snap-nodes="false">
- <sodipodi:guide
- position="64,64"
- orientation="1,0"
- id="guide4039"
- inkscape:locked="true" />
- <sodipodi:guide
- position="64,64"
- orientation="0,1"
- id="guide3935"
- inkscape:locked="true"
- inkscape:label=""
- inkscape:color="rgb(0,0,255)" />
- </sodipodi:namedview>
- <metadata
- id="metadata5">
- <rdf:RDF>
- <cc:Work
- rdf:about="">
- <dc:format>image/svg+xml</dc:format>
- <dc:type
- rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
- <dc:title />
- </cc:Work>
- </rdf:RDF>
- </metadata>
- <g
- inkscape:groupmode="layer"
- id="layer25"
- inkscape:label="Content"
- transform="translate(0,108)">
- <g
- id="g3856"
- style="fill:#3d3d3d;fill-opacity:1;stroke-width:0.81111115"
- transform="matrix(1.2328767,0,0,1.2328767,-14.376763,-8.863014)">
- <circle
- r="21"
- cy="-44"
- cx="64"
- id="path3874"
- style="opacity:1;fill:#3d3d3d;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:3.24444389;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:stroke fill markers" />
- <path
- sodipodi:nodetypes="ccsscc"
- inkscape:connector-curvature="0"
- id="path3858"
- transform="translate(0,-108)"
- d="M 63.421875,89.005859 C 48.016021,89.029098 33.691706,97.71624 25.455078,112.03125 29.673503,114.55644 34.640748,116 40,116 h 48 c 5.001681,0 9.660535,-1.26009 13.68945,-3.48047 C 93.502774,97.918077 79.022242,89.020453 63.421875,89.005859 Z"
- style="opacity:1;fill:#3d3d3d;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.40555558;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:normal" />
- </g>
- </g>
-</svg>
diff --git a/src/stylesheets/Paper/Light/Toolbar/settings.svg b/src/stylesheets/Paper/Light/Toolbar/settings.svg
deleted file mode 100644
index c0d8ed7e..00000000
--- a/src/stylesheets/Paper/Light/Toolbar/settings.svg
+++ /dev/null
@@ -1,95 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
- xmlns:dc="http://purl.org/dc/elements/1.1/"
- xmlns:cc="http://creativecommons.org/ns#"
- xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
- xmlns:svg="http://www.w3.org/2000/svg"
- xmlns="http://www.w3.org/2000/svg"
- xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
- xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
- width="128"
- height="128"
- viewBox="0 0 128 128"
- version="1.1"
- id="svg8"
- inkscape:export-filename="D:\Bethesda Game Modding\Mod Organizer 2\stylesheets\Paper\Dark\dots.png"
- inkscape:export-xdpi="307.20001"
- inkscape:export-ydpi="307.20001"
- inkscape:version="0.92.3 (2405546, 2018-03-11)"
- sodipodi:docname="settings.svg">
- <defs
- id="defs2" />
- <sodipodi:namedview
- id="base"
- pagecolor="#505050"
- bordercolor="#666666"
- borderopacity="1.0"
- inkscape:pageopacity="0"
- inkscape:pageshadow="2"
- inkscape:zoom="1.972807"
- inkscape:cx="-13.4715"
- inkscape:cy="-57.341097"
- inkscape:document-units="px"
- inkscape:current-layer="layer25"
- showgrid="false"
- units="px"
- borderlayer="true"
- inkscape:showpageshadow="false"
- inkscape:snap-page="true"
- inkscape:snap-bbox="true"
- inkscape:bbox-nodes="true"
- inkscape:snap-bbox-edge-midpoints="true"
- inkscape:snap-bbox-midpoints="true"
- showguides="true"
- inkscape:guide-bbox="true"
- inkscape:window-width="1920"
- inkscape:window-height="1027"
- inkscape:window-x="-8"
- inkscape:window-y="-8"
- inkscape:window-maximized="1"
- inkscape:snap-center="true"
- inkscape:snap-object-midpoints="true"
- inkscape:measure-start="8,64"
- inkscape:measure-end="17.9742,64"
- inkscape:snap-intersection-paths="true"
- inkscape:object-paths="true"
- inkscape:snap-smooth-nodes="true"
- inkscape:snap-midpoints="true"
- inkscape:snap-global="true">
- <sodipodi:guide
- position="64,64"
- orientation="1,0"
- id="guide4039"
- inkscape:locked="false" />
- <sodipodi:guide
- position="64,64"
- orientation="0,1"
- id="guide3854"
- inkscape:locked="false" />
- </sodipodi:namedview>
- <metadata
- id="metadata5">
- <rdf:RDF>
- <cc:Work
- rdf:about="">
- <dc:format>image/svg+xml</dc:format>
- <dc:type
- rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
- <dc:title />
- </cc:Work>
- </rdf:RDF>
- </metadata>
- <g
- inkscape:groupmode="layer"
- id="layer25"
- inkscape:label="Content"
- transform="translate(0,108)">
- <path
- style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#3d3d3d;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:6;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
- d="m 55.75,-99 v 4.125 7.809571 c -5.874077,1.121199 -11.426558,3.415826 -16.37915,6.767577 l -8.427247,-8.427246 -11.668701,11.668701 8.429933,8.429932 c -3.36362,4.949109 -5.670511,10.500514 -6.802492,16.376465 H 9 v 16.5 h 11.934571 c 1.121199,5.874077 3.415826,11.426558 6.767577,16.37915 l -8.427246,8.427247 11.668701,11.66870105 8.429933,-8.42993305 c 4.949109,3.36362 10.500513,5.6705116 16.376464,6.80249075 V 11 h 16.5 V -0.93457095 C 78.124077,-2.0557723 83.676558,-4.3503989 88.62915,-7.702148 l 8.42725,8.42724605 11.6687,-11.66870105 -8.42994,-8.429933 c 3.36362,-4.949109 5.67052,-10.500513 6.8025,-16.376464 H 119 v -16.5 h -11.93457 c -1.1212,-5.874077 -3.41582,-11.426558 -6.76757,-16.37915 l 8.42724,-8.427247 -11.6687,-11.668701 -8.429935,8.429932 C 83.677355,-83.658785 78.125951,-85.965676 72.25,-87.097656 V -99 Z M 64,-68.75 c 13.603902,0 24.75,11.146098 24.75,24.75 0,13.603902 -11.146098,24.75 -24.75,24.75 -13.603902,0 -24.75,-11.146098 -24.75,-24.75 0,-13.603902 11.146098,-24.75 24.75,-24.75 z m 0,11 c -0.478691,0 -0.949948,0.02335 -1.415283,0.06982 -6.488691,0.656052 -11.608841,5.776202 -12.264893,12.264893 -1.79e-4,0.0018 1.79e-4,0.0036 0,0.0054 -0.04613,0.463616 -0.06983,0.933021 -0.06983,1.4099 0,0.476656 0.02311,0.946126 0.06982,1.409913 1.76e-4,0.0018 -1.76e-4,0.0036 0,0.0054 0.656052,6.488691 5.776202,11.60884 12.264893,12.264892 0.0018,1.79e-4 0.0035,-1.78e-4 0.0054,0 0.463616,0.04611 0.933021,0.06981 1.4099,0.06981 0.476878,0 0.946282,-0.02369 1.409913,-0.06982 0.0018,-1.78e-4 0.0035,1.78e-4 0.0054,0 6.488691,-0.656052 11.60884,-5.776202 12.264892,-12.264893 0.04705,-0.465516 0.06981,-0.936808 0.06981,-1.41529 0,-0.478482 -0.02276,-0.949774 -0.06982,-1.415283 -0.656052,-6.488691 -5.776203,-11.608841 -12.264894,-12.264893 -0.0018,-1.76e-4 -0.0035,1.76e-4 -0.0054,0 -0.463773,-0.04671 -0.933243,-0.06983 -1.409899,-0.06983 z"
- id="path3915"
- inkscape:connector-curvature="0" />
- </g>
-</svg>
diff --git a/src/stylesheets/Paper/Light/Toolbar/tools.svg b/src/stylesheets/Paper/Light/Toolbar/tools.svg
deleted file mode 100644
index aae91d30..00000000
--- a/src/stylesheets/Paper/Light/Toolbar/tools.svg
+++ /dev/null
@@ -1,117 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
- xmlns:dc="http://purl.org/dc/elements/1.1/"
- xmlns:cc="http://creativecommons.org/ns#"
- xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
- xmlns:svg="http://www.w3.org/2000/svg"
- xmlns="http://www.w3.org/2000/svg"
- xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
- xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
- width="128"
- height="128"
- viewBox="0 0 128 128"
- version="1.1"
- id="svg8"
- inkscape:export-filename="D:\Bethesda Game Modding\Mod Organizer 2\stylesheets\Paper\Dark\dots.png"
- inkscape:export-xdpi="307.20001"
- inkscape:export-ydpi="307.20001"
- inkscape:version="0.92.3 (2405546, 2018-03-11)"
- sodipodi:docname="tools.svg">
- <defs
- id="defs2" />
- <sodipodi:namedview
- id="base"
- pagecolor="#505050"
- bordercolor="#666666"
- borderopacity="1.0"
- inkscape:pageopacity="0"
- inkscape:pageshadow="2"
- inkscape:zoom="1.6239214"
- inkscape:cx="-34.906444"
- inkscape:cy="-0.15941226"
- inkscape:document-units="px"
- inkscape:current-layer="layer25"
- showgrid="false"
- units="px"
- borderlayer="true"
- inkscape:showpageshadow="false"
- inkscape:snap-page="true"
- inkscape:snap-bbox="true"
- inkscape:bbox-nodes="true"
- inkscape:snap-bbox-edge-midpoints="true"
- inkscape:snap-bbox-midpoints="true"
- showguides="true"
- inkscape:guide-bbox="true"
- inkscape:window-width="1920"
- inkscape:window-height="1027"
- inkscape:window-x="-8"
- inkscape:window-y="-8"
- inkscape:window-maximized="1"
- inkscape:snap-center="true"
- inkscape:snap-object-midpoints="true"
- inkscape:measure-start="54,64"
- inkscape:measure-end="64,64"
- inkscape:snap-intersection-paths="true"
- inkscape:object-paths="true"
- inkscape:snap-smooth-nodes="true"
- inkscape:snap-midpoints="true"
- inkscape:snap-global="true"
- inkscape:bbox-paths="true">
- <sodipodi:guide
- position="64,64"
- orientation="1,0"
- id="guide4039"
- inkscape:locked="false" />
- <sodipodi:guide
- position="64,64"
- orientation="0,1"
- id="guide3935"
- inkscape:locked="false"
- inkscape:label=""
- inkscape:color="rgb(0,0,255)" />
- </sodipodi:namedview>
- <metadata
- id="metadata5">
- <rdf:RDF>
- <cc:Work
- rdf:about="">
- <dc:format>image/svg+xml</dc:format>
- <dc:type
- rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
- <dc:title />
- </cc:Work>
- </rdf:RDF>
- </metadata>
- <g
- inkscape:groupmode="layer"
- id="layer25"
- inkscape:label="Content"
- transform="translate(0,108)">
- <g
- id="g3997"
- transform="matrix(0.55904332,-0.55901902,-0.55904332,-0.55901902,4.4028105,-32.039863)"
- style="fill:#3d3d3d;fill-opacity:1;stroke-width:1.26487899">
- <path
- id="path3965"
- transform="translate(0,-108)"
- d="M -8,52 A 12,12 0 0 0 -20,64 12,12 0 0 0 -8,76 H 44 V 52 Z"
- style="opacity:1;fill:#3d3d3d;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:5.05951595;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:stroke fill markers"
- inkscape:connector-curvature="0" />
- <path
- id="rect3969"
- transform="translate(0,-108)"
- d="m 128,53 -18,5 H 84 v 12 h 26 l 18,5 18,-5 V 58 Z"
- style="opacity:1;fill:#3d3d3d;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:5.05951595;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:stroke fill markers"
- inkscape:connector-curvature="0" />
- <path
- style="opacity:1;fill:#3d3d3d;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2.86491537;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:stroke fill markers"
- d="m 32.291016,50.410156 c -5.753358,0.0068 -10.878972,3.636181 -12.796875,9.060547 h 12.796875 c 2.50182,-5.4e-5 4.529351,2.027478 4.529296,4.529297 -5.4e-5,2.501819 -2.027476,4.531196 -4.529296,4.53125 H 19.496094 c 1.917396,5.423518 7.042374,9.051114 12.794922,9.058594 5.30786,-0.0064 10.126181,-3.102522 12.33789,-7.927735 H 64 105.23759 v -0.0039 c 0.0293,0.0015 0.0586,0.0028 0.0879,0.0039 3.12696,-3.3e-4 5.66178,-2.535151 5.6621,-5.662109 -3.2e-4,-3.126962 -2.53514,-5.661785 -5.66211,-5.662109 -0.0299,0.0011 -0.0599,0.0024 -0.0898,0.0039 v -0.0039 H 64 44.626953 c -2.211168,-4.82439 -7.028882,-7.920713 -12.335937,-7.927735 z"
- transform="matrix(-3.8373246e-5,-1.7660262,-1.7660262,-3.8373246e-5,177.02814,69.028134)"
- id="rect3945"
- inkscape:connector-curvature="0"
- sodipodi:nodetypes="cccsccccccccccccccc" />
- </g>
- </g>
-</svg>
diff --git a/src/stylesheets/Paper/Light/Toolbar/update.svg b/src/stylesheets/Paper/Light/Toolbar/update.svg
deleted file mode 100644
index 0d017fa3..00000000
--- a/src/stylesheets/Paper/Light/Toolbar/update.svg
+++ /dev/null
@@ -1,141 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
- xmlns:dc="http://purl.org/dc/elements/1.1/"
- xmlns:cc="http://creativecommons.org/ns#"
- xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
- xmlns:svg="http://www.w3.org/2000/svg"
- xmlns="http://www.w3.org/2000/svg"
- xmlns:xlink="http://www.w3.org/1999/xlink"
- xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
- xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
- width="128"
- height="128"
- viewBox="0 0 128 128"
- version="1.1"
- id="svg8"
- inkscape:export-filename="D:\Bethesda Game Modding\Mod Organizer 2\stylesheets\Paper\Dark\dots.png"
- inkscape:export-xdpi="307.20001"
- inkscape:export-ydpi="307.20001"
- inkscape:version="0.92.3 (2405546, 2018-03-11)"
- sodipodi:docname="update.svg">
- <defs
- id="defs2">
- <clipPath
- clipPathUnits="userSpaceOnUse"
- id="clipPath4152-4-4-9">
- <rect
- style="fill:#fdf6e3;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:3;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
- id="rect4154-6-3-0"
- width="598.57141"
- height="928.57141"
- x="352.85715"
- y="22.362183" />
- </clipPath>
- </defs>
- <sodipodi:namedview
- id="base"
- pagecolor="#505050"
- bordercolor="#666666"
- borderopacity="1.0"
- inkscape:pageopacity="0"
- inkscape:pageshadow="2"
- inkscape:zoom="3.341656"
- inkscape:cx="55.620952"
- inkscape:cy="67.236268"
- inkscape:document-units="px"
- inkscape:current-layer="layer25"
- showgrid="false"
- units="px"
- borderlayer="true"
- inkscape:showpageshadow="false"
- inkscape:snap-page="true"
- inkscape:snap-bbox="true"
- inkscape:bbox-nodes="true"
- inkscape:snap-bbox-edge-midpoints="true"
- inkscape:snap-bbox-midpoints="true"
- showguides="true"
- inkscape:guide-bbox="true"
- inkscape:window-width="1920"
- inkscape:window-height="1027"
- inkscape:window-x="-8"
- inkscape:window-y="-8"
- inkscape:window-maximized="1"
- inkscape:snap-center="true"
- inkscape:snap-object-midpoints="true"
- inkscape:measure-start="18.8975,12.498"
- inkscape:measure-end="20,12.498"
- inkscape:snap-intersection-paths="true"
- inkscape:object-paths="true"
- inkscape:snap-smooth-nodes="true"
- inkscape:snap-midpoints="true"
- inkscape:snap-global="true">
- <sodipodi:guide
- position="64,64"
- orientation="1,0"
- id="guide4039"
- inkscape:locked="false" />
- <sodipodi:guide
- position="64,64"
- orientation="0,1"
- id="guide3935"
- inkscape:locked="false"
- inkscape:label=""
- inkscape:color="rgb(0,0,255)" />
- </sodipodi:namedview>
- <metadata
- id="metadata5">
- <rdf:RDF>
- <cc:Work
- rdf:about="">
- <dc:format>image/svg+xml</dc:format>
- <dc:type
- rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
- <dc:title />
- </cc:Work>
- </rdf:RDF>
- </metadata>
- <g
- inkscape:groupmode="layer"
- id="layer25"
- inkscape:label="Content"
- transform="translate(0,108)">
- <g
- id="g4217-3-45"
- transform="matrix(0.50121193,0,0,0.50121193,-6.8260092,-112.83314)"
- style="display:inline;fill:#008484;fill-opacity:1"
- inkscape:export-xdpi="96"
- inkscape:export-ydpi="96">
- <g
- style="display:none;fill:#008484;fill-opacity:1"
- transform="translate(0,-783.36216)"
- id="layer1-9-66"
- inkscape:label="Reference">
- <image
- width="269"
- height="269"
- preserveAspectRatio="none"
- xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQ0AAAENCAYAAAAVEjAIAAAABHNCSVQICAgIfAhkiAAAIABJREFU eJzsnXd4HNXVh987s129WZJ7t+VuA8amGNOLA6GXQEIgQAotBAghJAQSEgKEJAQSekvoYHpJ4ANT jcEN27hXucjqfbVlduZ+f8zMatVsrSRbtjzv84iyO3PnzuzMb84599xzxaJSKXFwcHDoJEpvd8DB wWH/whENBweHpHBEw8HBISkc0XBwcEgKRzQcHBySwhENBweHpHBEw8HBISkc0XBwcEgKRzQcHByS whENBweHpHBEw8HBISkc0XBwcEgKRzQcHBySwhENBweHpHBEw8HBISkc0XBwcEgKRzQcHBySwhEN BweHpHBEw8HBISkc0XBwcEgKRzQcHBySwhENBweHpHBEw8HBISkc0XBwcEgKRzQcHBySwhENBweH pHBEw8HBISkc0XBwcEgKRzQcHBySwhENBweHpHBEw8HBISkc0XBwcEgKRzQcHBySwhENBweHpHBE w8HBISkc0XBwcEgKRzQcHBySwhENBweHpHBEw8HBISkc0XBwcEgKRzQcHBySwhENBweHpHBEw8HB ISkc0XBwcEgKRzQcHBySwhENBweHpHBEw8HBISkc0XBwcEgKRzQcHBySwhENBweHpHBEw8HBISkc 0XBwcEgKRzQcHBySwrW3DqRrEUqKN7J9yyaKN6xhS/FmYtEoUsq91YV9EsXlwuf1kZaaQkZGJjn9 CkjLLSQ3L4/C/gMJZOT0dhf3eaSh01hdxs6SEioryglWl1JdXUVDfT2NwSZC4TCGrmPoMRSXC5fb g9vtJjMtlfSMDLKycwhkF5Kfn09BYSGBzH69fUr7NHtMNKLBWjZv2sjObVvYsGo5WzZvpL6mimBj A5FQiFhMwzAMOMBFAwAhEEKgqioutwev14cvECCQkkpmVi6jxoxh4PAxDBk+kvyBQ1Fcnt7uca8S bqhha/Fmdm5Zz+YNa9m8aSPB+jpCTUFCoSa0SIRYTEPX9V3eX0JRUBQFt9uD2+vF5w8QCKSQmpnF 6NFjGDxiDP2HjmTg4GG4fSl78Qz3bcSi0p59andsXse7r77A1o1rqamupKmxgVBTkHCoiUg4jBaN oMdiGNIwRcMBACEEiqIghILL5cLlduNyufF4fXj9fnz+AGnpGaRnZjNi9BhGjp1E4eChDBo8BHcg o7e7v0dpqC5n86aNlBavZ+3K5ezYVkxDXS3BxgbCoSaikTBRWyhisU7fX8ISa0UoqAnX3O314vOZ 1zyQlk56eiZDh49g9PjJ5A8axrARo/Gl9u1rvit6TDRkLMr8j9/nvbnPUbJtC3U11YSaGlv+mLqO YehIKU23xLEymhHC+lfzjSwUBVVVUV0u3G4PLrcHnyUgPn+AQGoaaemZDBwylKLxExk4ooiho4rw +AK9fDLdo6a8hM3rV7N94xpWr1pJTVUF9bU1BBNeQNFIGC0aRddj8XtLGgYSmdz9ZV3vxGuuKJaI uNx4vF48Xh8+fwB/IIVAahrpGVnk5RcybsIE+g8by4gx40jJyN7zF2YfoUdEo76mijeeeZgFn82j rGQ79XU1hENNxDQNw9DjbogdvzjQ4xi7Q1gCYv+3UBTLElHjIuJyuXF7PLg93vgNnZqeQU5ePmOK JjDxoBmMHDuOQGZeL55J5zBiGts2rWXdiiV8u3wZpTuKqa2uosmyJCKRcFwk7BeQoevN1kTCvQVd u79aX3Msy08RCkqCcLs9HtP68/rwBVJITUsnIyuHQcNGMH7SVMZMnEr+oOEI0XfHGLotGuuWfcUr /3mC9auXU1m2k4b6WqLhMDE9hrTMQ0ckuo9IsETau6FdLjdenw+vz48/JdV6GxYw5aBDmTxjFiPH TgDV3ctn0ZKash2sWPg58z+dR9nO7dRWV8Vdjkg41EIkpGGgW1bq3noBdeaauz0e85pbVkh2Th4D Bg/liKOPp2jKoaRm5e6x/vUW3RKN9cu/5t8P3cem9aupKi8l2FCPpkXNH9kRij2ObVYn3tC2b+7x +gikpJKalkFWbh6jxo5n9gnfYeyU6bh70X2RukbxxrV88eF7rFjyNRVlO2mwXI9IOISmRU0L1bIk Wr94evO+ai0iQghURUVRVfOae7x4fX4CqWlkZuWQV9Cfgw8/ihlHHkv+oGEIRe21vvckXRaN+poq /vmnX/PtNwup2LmDYGMDmhY1/UpHMHoFW0SEoqAqlknt8bZ0XcZNZPaJpzLh4MNQPb691jepa2xY uZSP//smq1Z8Q1V5GfV1NTQFG+NWRWtXdn+4j1oLt6q64tZHSlp6PP4x5eBDmXXCqQwcMXa/F48u iYahRXj2oXv56L9vsXO7Gcl2BGPfIh7cU1VU1YXHa4tHJrn9Chg3aSonnnY2oyYfusf7Urx2Oe+/ +TIrli4yXdi6WpqCjUQjYWKahm7o+5VQdESigLjUBIvPCp72KxzA9MOO5Og5Z5E/aHhvd7fLdClP Y/7H77Pgs3lUlu2MuySOYOxbyISH0NB1YjGNSDhMU7CRxvo6aqoq2LBuNdMPn82cs75HRl7/Hu9D sKaC9996mfnzPqBkezF1VnAzEgmj6zEMw+hT902i6GmGQUyPEY1GCIeaCDbUU19bTWV5KcuWLeHY E05l1gnfwZOS3su9Tp6kRWPH5nX899XnKSvZTmNDnSMY+zjxG9kwMHQdXY+hRaPxG7mmsoLVK5Zy 8hnnc9jRJ/VIsNSIRVn21We888qzbN6wlsryUhrr6+IxC/t+6cv3jJQSaQ0D26IdjYQJNQVprK+l unQnyxZ/yWnn/ZCR46fsVy5L0qLxztznKdm2hfq6GiKhEIau9+kfvy8hpWweidBjaNZbsLG+jsqy UlYu/Zqzvn8F2QWDunyM+spS3nzxKb76/GPKdmyjtqaKcFOQaDQSv1cOpPuljcWnaUQs8airrWbH 1mJmn/gdTvjuufhSM3u7u50iKdGINNawbdM66mqqzTwMy8R02L8wrDe9YRjosWYTurG+jq1bNnHO RZcxaebRSbe7ctEXvPbcE2xYs5Kq8lIaG+qIhMPEYtoBb402i4eGIc3rHgmb4lFfV8Pmdas5/cLL GDJ6fG93dbckJRpbNm2ktqaKUFOQmKY5GZ37Mbb5rFn5DzHNNJ/DoSYef+AeDl+1nFPP+yH+TqRL h5sa+b83XmTef99ie/FGaqoqCAUb0bToAWld7Arb4tAsa892W5qCjZSWlTLn9HOYccwcXB5vb3e1 Q5ISjdLtxWbgMxqJp4M77N/YN3HUCDcHTCNhGupq2bZ5E2df+EOGFE3tcP+dm1Yz99knWbZ4ARWl JdTXWtnAjnXRIYkuS9hoQo+ZcaZIKERdVQWb1q1mzjnfJ6dwcG93tV2SEo11q5YTagqawSznZugz 2DdwLKZhNFkui+V3l5Vs48yLfsT0I49DcTe//aSusWT+x7z+/JNsWr+GmspyGhvqiUbCjnXRSewA tWbFe8wRrhDBxgZKtm/lzIsuY/SEabCPBUk7LRq6FqF480Yi4VA8mObcFH0LO9YRNgxi9tsvHOLp B//Gum+XcvYPrySQnkU42MBbzz/Gpx/+j53biqmtrjRd1pjmBMaTpEWso6lljKmqopyTTz+HI086 E7d37yXi7Y5Oi0ZJ8UYaaqsJh0JmnQKHPokd64hJSVOj6XdHIxGC9XWU7yxhzrkX88EbL7B04QKq yktpqK8166PozoukO9huopnDYsaYtGiEuc82Ul1VxZyzL8KfntXb3QSSEI1tWzbS2FBPzMnLOCAw rY6W7kptTRVLvvqCYLDRnKKeMJTqjKJ1n0TBDhkGhqGjRaO89/qL7Ny2hXMv+Rn5g0f0djc7LxrF G9aYs1c1c8jIoe/T4u0XixFqClJbXWnmecRijnWxh0gU7HhCXiRCJBLmwh//nMIho3q1f50Wja3F xabPKnsmm++oPz1D2pAxAIjdbNuXkYAWCVNdup1YTSXh0q1Ey7dSU7aDph2biVTtjM/07JX+JQzN xmJavFjQvjRXRHF78PYbiC+vP9n9BuDrNxBXbiHurH54MnPIzm1Z81MaBlXlZWh1VcTqKtHKtxIq L6GqdDvRqp1Eyrf36jWHloJt1Brous7ShaZFd9FPrqP/0NG91rdOi0ZMi5rVkXroJkntP4zMoWN6 pK2+QN6YyS3+PxJqQmuoIVxTSemyBVQv/5zK9d8SKtmCNPZ+TGlfEQgAlz+FlOFFFIyaRPrEwygo moIrJR1XIB1voHPT/rOGF7X5LNTYgN7USKypnu1L59O48itK168gVLwGXdN6+jR2S3w4XEaQ9XVI KVm2+Ct46G+9KhydFo0W9Qz2kZunL+P1B/D6A6T2G0DumMkYZ11OqKac8vUrKPv8HXYu/YLQtg0Y vSAgvYEnLZO8STPIO+hoCg6aRWrBIHypPTvZy5+aBqlpQKH5QjvjEoK11dSXbKF86XxKv/6A2hUL 0CKhHj3urogPy2pRgg31ACxb/BXKI/fx/Z/8oldiHMnNPXHEotdQVIWU3AKG5RYw9NDjCNVUULpy IVs/eInSBf9HrKmht7vY4yhuDxmjJzP0uHMYPPN4/IWDcO3lSuwpmdmkZGZTOG4a486+jKadxWz6 v7kUf/IWjZtXw16I77UWDiEEyxd/zctPPcilP7+FwF4eVel0PY07rr+c5QvnU1qyjXBTsNvR8jlP fUHWqIndasMBjFiM6k0r2fDWv9k273UiNRW93aVu4/KnUjjzBAaeeAH9J8/An7bvVf4O1lRS+e3X rJv7CBVLP8OI7Xn3xS6w5PF4Sc/MonDQUE4+/Ty+e+HlezWPY68tltQGx2jpERSXi9zRk8m57h5G n3kZ615/guL3X0arr+7triWNy59K/yNOZszZPyFnzGRc7n13fZeUrFxSjjyF/ofMpmzZAla/8AAV Sz/F0KJ77JiJFkdjfR3lJduZ9983ycrO5tjTzt9rmaO9JxoH8pDJHkAoCtnDijj02rsYdeoPWPn8 /Wyf9wbGXvS/u4ri9pA97SimXHwDueMP2usuSHdw+wIMPPQY8ifPYOeiT1nzwv1ULJu/x4LV8eBo NEJDfS07txfzvzfnMmj4aEZPOmSPHLM1fbfO+gGKUBRyRk7kyJv/xYzbniB7zJTe7tIuSR82hkN+ 9U9m/+FpCibP3K8EIxG3L8DgI05i9t0vMOnKO/DvgUpoNvHh2FCI2uoqijetZ+5/HqV659Y9dsxE ek80HPdkjyJcLobPmsNRd7/M6POvRvWn9naXWqD6Uhh1xmUcdfcrjDrpPHwp+1b/uoonkMbE869k 1l9eZdDs7yJce2bZCMOaWh8ONVFTVcHqFd/w9sv/QYuE98jxEuk90XDck71CSm4+h/zsD8y4/Umy Rkzo7e4AkDZ4JAff8iAHX3sXGf2H9HZ39gh5I8dx2G8fYdJVd+LLyd8jx7BnJoeCjVRXlLHgs3ks +PCdPXKsRBz35ABAqArDDj+RWXe/SP7hJ7dYTWyv9kNRyJtxAkfe+QKjjjkd1b1vLd7U07h9fiae cwWH3/EMGWOn9vh1l1IiDYNoNEJjQx2lJdt4782X2bx2RY8epzWOaBxApBUM4ohbH2Ps93+B4vXv 1WMrXj/jz7+So257nOxeTIHuDQonHcrRf36Rwadc2OPuSmJ8o76mmi0b1vLGs48Taqjp0eMk4sQ0 DjD8qWlMu/y3TLvuL7j20srnrpR0Jv7090y84jZ8+2DOxd4gNa+AGdfdw4hzfobSw6X8pJToujmh sKaynG+XLeb9119C6rEePY6NE9M4ABGKwpjvXMQhtzyMJ3PPrjXqzcxl6g1/Z/yZl/V5d2R3uP0p HHzFbxlz6W9w+VN6tG07vhFsbKCitIRPPniHdd8u6dFj2DiWxgGKEILhR57MjN//B19u4R45hjcr j6k33s+o485CUfetknW9hcvjYfIFV1J0xe9wBXpuxCiev2HVdy3ZtoW3X/4P4cbaHjuGjWNpHMAI IRg07TCm3/5UjwuHNzOXqTf+gxGzTkFRnB87EZfLxfizLqfosltx9eBQeDy+EQ5RV1PF2pXL+eT9 t3qsfRsnEHqAI4Rg8JQZTL/1cVxpPTPxSQ2kMfkXf2X4kacgHMFoF5eqMv6syxj9w1/2aIzDdlOa go1Ulu3kk/ffobR4Q4+1D45oOAAgGHzQ4Rxyy0OogbRutaS4vUz86e8ZOftUx8LYDS6Xi0nn/owR Z/+4x0ZV4m5K2HRTtm/eyEfvzO3RtHZHNBwsBCOOPImp1/wJ1dW1KUlCURh/3k8pOvUHTgyjk7g8 HqZdejPDZp/WY3kcUkp0QyccaqK2upJFCz5n24ZVPdI2OIFQhxYIxp56MUUXXtelG7jfYScz7pJf HfCjJMni9qcw8ao/kT6m40WpkkXa9TcaGyjfWcIn/3urx6wNJxDq0Iaii28kb8YJSe2TNmQUh157 F25f58rtObQkLa+Qg675c4+lnCcGRevrali2+GtKtvRMbMOxNBza4PX6OPxX95PWf2intld8AaZc exfp/ffNZQT3F/pPnsGYi65H9NBMXymlWUU+2EhleSlfffZhj7TrWBoO7ZKSW8CkG+7rVLr56LMu Z9DBya8y79CWsaddzMAjTu6RtuzYRiQSpr62miULPqeuurzb7TqBUIcOGXbo0Yw598rmZQvaIX3E OMaed40T+Owh3D4/RZfchL9fz9TjkIZBLGrHNnawctH8brfpiIbDLhn//evI7KCWq+LyMPnHt5Oa k7eXe9W36TdyAqPOvQrRQ0KsG1bCV201Cz6b1+2A6H4jGgJQAb8qSHXt+i+gClQ65wGpgFdht22m ugRqL7tUCrs+/2TOu7P4UtKYevVdqO0kIOUdcQoDpjtuyZ5g7HcvJmfC9G63Y0+f16JRmhob2LF1 Czu3buxWm/tFIFRgPiyZHkHAJfCpAr8qSHG1/POr9neQ6TE/60irFUyhyPCYD1xHbQasNr0KZLrN bff2RbPPP90tCKjE+xpo1ceAS8TPuyeFY8C0wyk86vQWn7n8KRx0+S37dPHf/RlPII1xF1zbI9mi iSMpNdWVrFuxtFvt7ReBUFsIFCFwCXAL8Cjmn1cReBWBRxF4FPM7lyJQhPmgp7nbPkAK5udeBVQh cClmu4ltxttTwNWqTf9eNjlcCvhVsw+qYlo8butc3Urzf6uCeB9TXD3bx4N/djvehBmxQ0+6gKxB vbumaF+n8JDZ5E05otvt2AHRaDRCU2MD3+63otFJS0NgPrRCCBTrwfBYb9pUl0K6W5DhFqS5BSku BZ8q4g+QKswHLNHisAXDZT1gijDbt9/UaW6FDLcwrQq3QkAVeFVhPZDWQ6nSoQXT06gKpKgi3lfT nbItC4WAquC3+uhK6KNL9KybktqvPyPPvAKEwOVPYcy5VyHU/ca73S9x+wIUnXcVak8UTJKSmKYR agqyo3gzDd1YH2efX8JAsd7+ccFQBLGaMoQWwq2AUE13wVyoGDwZebh8KUR00AyJROBRJC6PQNPN t7aaIBgexRQMTzRMpKYUYVk0ErM9zQDD7ceTlU/UkEhASEh1CxpiEtEJ8ZNAMktLKdbl8VhukWIJ gAK4FYESbkSrqwBVIIRANyRR4cKXNxAMSQzi1ki0BxcAG33m5VR//haFR8whc8DQnmvYoUPyp8wk d+JMyhZ91K12El2U2qpKNq1bzeRDuxbA7j3R6CQi4d8uYb5hn//9Nayc93a7288851LOv+3vNAgX TTGJYYBEgJR4VLMdAXHB8KsKaS7JB/ffwbuP/q3dNicedyrn3vschlDQpTStHiQZlusT1z/R7ApJ iC9jqUtTfGJSEjPaioiCKY5ea2jTFkm7PYEtAub5r/30HZ696dIWfcwbMpLr3llOLAa6vmcy51Ky cph6+W/ImDrLsTL2Em5fgNFnXkb5kk+6PephSDMgGmxsYMemNUw+dFaX2tkvfnnzITUfHJ9q+u8d sfit51g1710y3MRNdlU0i4UAVCuG4VME6W6o+vYr3n/6Xx226bECjS7RbAWY/RHNMRHFdIvsP4/1 kLsUYbpT1ghHhscMaKaoZgAzxQpwZrjMbQIuc/sW7SmmheVTBWluSHO3/dmEMIOkLqtvyh4qHpw9 8yTUHq465bBr8ibOIG1Y21Xuk0FKiWEYxGIa4aYgq1eu7HJb+4VoQLN14LPfwh0QDYd5577boLrU Cnaa8QxV2HEB8//diiDFLfCEG/n3b68mFo102KbLOq6qCCtmQKvgqTWaoTY/+H4r7uJVBR5LRMx9 zPiIVzUF0NcqwOlJEIiA2txuiluQ4YZsj0KGp+0FEJhulqrsMher+/RSJfMDmUB2LgNnzel+Q1Za eSQSprKilKb6rhUf3ucDoTYi4WHd3Vt0+/o1zL3vDjJUg4BL4FKbg6iK9eZOcQnSVMmHj9zF1rW7 Vl1VWIFGpVks3FYsJMVlBk6zPIIcryDPK8i1/rK9ZkA1za3gd5kCZrfRQsQsi8KrCgKqGdzN8pj7 5/kEeT6Ffl5BP59CjkdpNwgrbGFynuk+yfDZp3d7MmBiScD62ho2b1rfpXb2+UBoy13MB6wzL7v5 rz/HpKNOpOi409CkREowpLmvV1VIc8HO5V/xxuMP7LYtxX6oBcQUM7ZixhfAK2OE6qopW7+C6q0b KV6zEl3XycjOYdDYieQNH0PGgCFIXzphHZp0iBrNsQ3TXTJHRAKqwKfoaJU7KVu3gq2rllNRsg2P x8vIceMZOGI0g8ZOIGa0XaHcDIHYDpgzG7CvkTJwOJlF06hY+nm32onHNRrqKSveyPgpySeQ7Zer xrenGUJRkEZzeDEaCfPqvb/h19MOIT2jAEOCbpgPaIYblHADT/32mjZuSet2bDwq+AzAHnUxYmxf /DkfvPAEKz/+H+FgQ4f9HTx6HDPPuJDpp19EZmYewZggokt0aQqSVxWkqpJw6RZef+I+vnr9OZoa 228vt6A/WXn9On1dHPoGbp+fftNmdUs0EuMaoaYg61at4JjvXpB0O/tFcldnmHbkMWT1K2jx2faN 63nxb38gyyUtN8HM6chQJR89fDdb1nzbYnuP18fUI2a321W/5YpkewTuujJeuu1q7vnhaSx+55Vd CgbA1nWrePGuW7j7/KPZ8slbZLgMM6/ELUhzCTJdsO3z97jne8cz75mHOxQMgMrSEtav+KbT18Wh 71Aw/dh20/mTwo5rhEMUb95ItGnX92577DeB0N0xeuohnH3VL1GUlh7//NefY+mHb1PgV8n3KeT7 FEpXtOOWCMF3r7iOooNmtGlbYo60ZHsU1PoK/nH5GXz68tPoSS5GU1a8iYevv5Tlbz9PjhtyPWYM pG79Mh7/9c+oLt2R7Gk7HEBkDBiOv5tZuHaNjWgkTEN9Ldu2bk66jT4jGqqAOd+7lImzjm/xuRaJ 8PLdt2DUlNLPp6CG63n4N9egtXJLRkyYxPevvbHD9hUBXqHz6B9vYcO3y9p87/Z4ySoYwEFHHcf0 Y09ixITJBNLS2pTNCweDvPinm6hdvZACn0qaKpn799upq2xb58AXCDBi/CSmH3sSRQcfSiAtHUXp Mz+ZQ5IEsnLIHzm+2+0Y0iAaMVPKd3ahmtc+n9yVDIFAgCt/9yd++e0SahMewu2bNvDQnb/jprsf 4JX772LL6lZuic/Hz+64H99uFq9Z9uWnLHjzBaRsGZAZMX4SF954O0VHnoDH5UKXpojV7djEy089 xrxnHiLcFIxvX19Tzav/vIebH36GdYsWsurLT1oeSAgmHHY0l/7mT4wYPxUD00WqL9vBe6+/zDv/ upv6mqouXSOH/Zu08TPgfy92ef/WcY3ijWtJdnbLfjPk2lkGj57AOdfc3OaNPP/153j0zt/y2mMt 3RIhBKdffh0TDzHdknaDrNa/P3j7DWJay5GLwaPG8vunXmPi7FOo1xW2NulsbdIpCem48ofxk9/8 iXN+cStqqxL1y776nO3FW1m38DMioVCL7/oPHc719z7CwKKplIYNtgZ1doR0RE5/LvzJdfzwzgeT vzAOfYKBk6Z3v+CRnVIeCbN+3bp45nJn6TOBUICYAWFdcuL5lzDhyFZuSjTKaw//rR23ZDIXXHMj wZgkupss3Q1ff9Lms1N/dDWp+QOpjBrURCW1UUldVFITheqoQVCXfOeiH5HTf2CL/RrrainevIm1 Sxe1aXPWnDPJHTCYWs2gNiqp1SS1UaiJGARjBmmFQzp5RRz6GmpaJp7c7lf10nUdLRIhWF9LY01Z Uvv2GUtDAo0xSU3UoEn1ccWtd5GZu+sJOV6fn8v+8A8a1QC1UYPGmNFut+zPStsJGg0fPYaYhJgh iRqgSXOuSUxKNAOiOqSnpeP1tK07IYPVVJe3/cH6DRxktmFIYtZQcUxKs22cLIwDGVdKBt6cgt1v uAvswjyxmEZTU5CdJSVJ7d9nLA1DmlZGY0xSGzXwDh7DGVff0uH6HUIITrrsWvInTKc2atCgSUK6 +cB3xMARY9p8tmb5N+ZcE2tGqp2V6VYEbsVMF6+uriIcaZumrmbkkz9wUJvPN3y7HJeQVl0PM4/D ng7vVfZ+ESCHfQd/WjqZ+d23NAxpENM0wqEmKiuTmybfp+6/qCEJ69CoQ70mOeTMiyk6sv31O4ZP mMwpV9xATdQgGIOoBF1KFEXF5fHG/8w8VJNRh85u084bj95HxZZ15HoUsr1m+neWV5DtgVyvgk+R vPro/VSVbGuxX2ZOHsOGDWP4+Elt2vzi3ddYuewbMy3dZ7ab4xXkegQedHas3v/zNATECxq19+dR nGS1jvDndV80pJRxFyVUXZrUvvudaEjaN88lpukekxDRJY2aJKh4OeeXd5CWlYPqcqO63Hh8flIy sjjjprsJuwOEDXM/MOeYBNIyyMgrIC07D39aOqnZuXgCKQjg5O+egcfra3Hc8h3buPmCU/jqrRfx aU0M8EgGeCR5Lp26DSu45+dX8Pq/7sbQWwZMph4xm8IBgxh3+HH4U1pvlZHxAAAgAElEQVSO2tRV V3HPFWfx/lP/JFpbRT9VwxNuYNNXH3HXVRfzn99e3eG12R8IqKa4plrlClNdZuJdmrv5szSXuc3e rpK2P+DO7976MlJKaxRFR9OiVFVVJrX/fjXkKpEYUrQZ8gTTStANiWaYvr+UoGsS79Dx/OL1Rfgj 9eabSwCKQmDACJr05oLFXsuV6JeVxrjhg2kIm6MkwWCQ7OwcXIpg3EEzOPHSq/ng6QeJRsIYVnJX +Y5t3H3VD3B7vQweOQZVdVFdUUblzvaTtbLzCzjryl+iqR5GTj2UyUedyIJ357bYprJkO/+69Xq4 9fpOXhswpETu49IRSCjdKIQ9jb/ZqpDCdDUNIZASAqo5Qye0h2qE7I+4M3J6pB17+LW+ri6p/fab uSeGNeFMMyRGO/vqhlmlSjdAN8x5HYYAIwberHyEko/bKtOnWO15LTvLnoaeqirk5/WjqMisXdDU 1EQ4HGbkgHwkphVz6XU3U128nm+++BQtGiYaDsWHrLRIhI0rl+/yPFIzMvnB7x/AN2wCO0M6LkVw 5vW3sX7pV1Tt3J7cRUlASuv8E66NWxFE27tYvYQ99T+xdKNLEbgwyw5guYgxK5Acw3QOvYo5stVz 657v37jSe0A0LGtDSkldY1Nyx+/+0btIklanxHwgwh0EKyPW6IWOKQj2Qx5XGCEQ1oQ1tyrwClCR NNZW0lCymdKaKratW8WadesZniLITE8jKyuL1NRUMoaPBSGIxsDwpnDVvU/w5pMP8N7TD6GoLrRw CD3WduZpy/MVDBxVxA9/fz8FU2ZSGZHEpMQlINB/BD/914s8dsNllG5cndyFiV8fSciQ6EbzsLvb Km24L7yk7fiFalsXmKLmVyQ+q6QjmHGpkC4JSwVpdVwVZl3XBk06wgHk9eve6EkLpETb3b3biv3C 0pBSIoVAl5KwIUwxaEVYh5AuiRkSA/PBsQdOFMwRjYAKKYpky6JP+PTNl1n20TvtDnl+0er/Dzro IKZv3Mrs08/FlZEHvgCn//SXzDj9Qt5/5hEWvv4sVeVlHQrH6KmHMPvCnzDlpLMJK26qo6bIGVKi CIgYgqwxU/n13E/5+D8P8unzj1JTXopoJ2V81PhJ9Bs3jU+efbjVNRKEdFMoTeGUCCC9VTX2NqNJ sqVDE0p4KjUpcbW3TytsCwKsd4G9fULbiccQwqzV6lUED11wJNtXLmnR3ojps7j4sf+a1qVhuicK kjS3INKDNU+BeG0Us1/N52m7wGaZRvMzzSrXqCrg6WYxooguk6obuyfRo9Gktt+vLA07eeuCf7yE P6HsX1RCKGaKht5sWMQrdPldggyXZOeSz3npnt+ydunXSR178eLFLF68mH/fcxtnXHY1x/70V3jd bjLzB3De9bdz9i9up2bTKsqKN7Bj03oM3SA1M4vBo8aSM2wsrowcQjFJvQ5hTRK13ScJQpjulpTg d6VwzOU3cPJPbqR+2wbKN66mZPMGsrIyKRw2mn5DRpKWVwjAT+64D01CVJeEdUkwBsGYRLdudiEE ipRm7MC63u1ecuvml9Y/UlyWSAN+aw8R37Tj4d52n6GEts16JmZ/7HKEXqX9SLxilXXUDNPFNNsw T8DThdC9fU1aCFfi+bR3bYRAYoqK+RIyi1Qn7g9tr4m97e7wqwJdQoPW++KxWyu5Fft8IDRmQMQA n1VfRtNNPyyq2xW8zAdQt5Kp7B9AwXRDUlyCDBHjowfv4pX7/titvgQb6nnmb3/kkzdf5qbH55I2 aBRB3XxQvUOKGDa0iJGzzW0ldpxF0hC1Er0ssZBWoRxFWA8TkqhhVkDXEQSkJDBwJKMHjWTcMebt aUhz3+qoEd9HNyAGaLoZz7DPXWDVRbVuaLvqmS0g9h9YNznNwqUnBCFlQnuJQUsbWyhEwjYCqzo8 zcFZAzAM0K0d7GUj3B2UJlQE+BXQrJETRcp4AaVksM9BkSIuXHbhIyGar1PitYnva10Tw7om0rom 9jygjq6Jec6i5bVL+C7+J0EgSXcL6ntZOFpPcdgd+7xogJkZKRXzYYsJK0MSibDWDzAkcZcE7LKA ZqQ+XWi8f//tvP5w+5XGu8K2jeu44wencvNTbxAYNBpNN/tju0E2hrTcBatfLiFQhMQtwUC06Ld9 E9thGNslo8X7zdzGQMb3UaR10yugSjAUgTAkCFAQ1kJPZt1Rl1Vf1S6ODObxdSniWadRw3zD69aD Kml+sFyiuQ17JFRYeSyKSPiz2tYB3RBxwdQSrA1VmIWT29MBBfC5BFFrW/v6qaLzK8cZmPeNIc0s 3Zi1/ISkWSRs4XJbRZyVhNwQXZr9jhmm4GvSDLAbdhuY19Wu+WoLhy28iaJhY8flYtKOzVi/r/3D 9xKKKzkZ2C9iGmEDiEkCLnPNESFEXPGRrZYDsG5ojxCkuGDRa8/zxsN/a3dSjhCCsdOmM2rGbCZM nkr+oKEoLpWG6iq2rV3J8hUrWPb+6zTW1bbZt3TrFu67+gf8+sWP8HkDRHVpLtCkND8MEqyHUCbE Y83bSLcEJWY9oFgPkU8Bv0ug1ZRTsnktW1avIBwMkp6Ty7CiCWQNH0dOWpppWWkaaxbOJ2PEBNIz s2jC9PljYJnyZklCnwJaQy2Rih00VJZRWbKN2spyfIE0Bo0cjTctk/ScXPyFQ4jokqaYGSPSZPMD 4lXNtvyqGYtQhVnE2DoUoWCQmuJ1bFy9kuoyc6i5YMhwCkYUkTF4NNLrMpPorDKHbsWMDdz75ucE Wt2FOlAXE5a1JhFCia9509m0jZg10haKmZm+YYN4iUW7rYDLdBNCtdXUbllL6ZYN1JTvxO0LMHzC FDKGjiUrO5eQdU0iuh3fMMXMo9jLZJq/uyBhhI/mqvVCNL9Awrr5FzGaX3h+VdDYXqCuAyrKk0vG 2iVC4EmysM9+E9MIG6BFJR61pSwrmKX47P8WVrFen0ug1Vfzzr/ubNfHHD1pKj/+69MMGj6SoKEQ 1pvjARmDBP2mHcVhF4L84/189MS9/Pvu32O0KgO4aeUyvpz7NCdffCVRl+TaI8ZQU76zxTazTjuX a//2OGFdsmXVNyz5+H1KN6yirGQHW1ev4IRLr+WUK39lmsp6lDWffcC7T/yD1Qu/REoDQzffbUII FEVBKCojJ0zm4OO/gwik8+wfbsDj8zPtuDkcet6PGTB5Broi0GMaa//vTdZ8+CbrFs2noaoCaRjW MJv5b7N0ofl6FUKQnV/IhOO+y7HnXkz2iPGEdPOmVgR88dR9vPP337W5jpfcdi/fzHuP5V98TCwa xTD0+PUWioKiKKRmZDH9lLM49kfXkpk/BE1ay1EogpvPPprNK8xAqO06TJw5i9898w6blyzgtgva z+hNhkBWDje9tRh/aiZgxkVcGCx95yUWvPwEa5d8hWEY8esDAkU1+z566nQOO+9yJp9yDlHVFGUp Tatn0YuP8Pqff9nmeA9+toq8/oMor65mxacfsG7RfCq2baF0WzGarnPpY+/izilE6jLuYidDrL4H yiIIc6EtIQQZqckVLN4v3BMbHdpN8olJCLhE3Id3C/ONuPyD1yjfXtxm+yknncVNDzxNBBclEYMm 3TDjIVYAUMEu9gtpbhdzfnwTBVOO4p7vn4Deamr8h88/wakXXExqShpS14hpLSPRkaYGPn7hcV55 5O+UbNmEHmtZ7SvdJenvU6mrq+HJ237BR6+90CZ7FJrTftF11ixdyJpvFqEIgWEYhIONzH/jRRa8 PZepc87lorseAwmL33mZb//vzY4vqJQYsvlYFTu2Me/pB/j02Yc589pbOOEnN1nDwgKfMNqcG8Bj v7223ZqqANIw0A2DuqoKPvjPQyx5/00uveshxh1xArqU+F0KRiyK1qpdPaZZ82xku8dMFqlFyXPp uDzmC2Xnum956PpL2Lp2ZQd9N6eOG7rOqq+/YPWiLxn6xN+44q9Pkzt8DFHdzC8JdNC/0rUreO7O X/HlB+8RCYdaHCNv8DB8KmZ8rotuiVbXM7VU7BdRekZGUvv1iVmuEcsPB2vURBEosSjL5r3bZtvM vAKuv/PvhFGpiBhURyT1mqQpZtAUM83ZYMygUTOo1SRVEUl5VHLIzMM448e/aDP8uGXtSjZt29bh Wixf/u9t7vvVVWzbsK6NYNj9NSJBHv71lXw497l2BaNdrGy+RFxuN0edfr6Ziu3zcOyZ3+tcW63Q Yxqv/PX3LH7hQXNOjUch0MHaCB0JRntUlZXw799cRV3pNjLcCqkugdKBySlEDy7HoLrwZOSS61Oo WLWYv1xyKsWrV3S679Iw2LxyGX+5+BTq1q8g36eS51VI6cBXuu1H5zLvjVcINwXbPYYdA+nq6Wll W7u4p4ltYSiKitvtITs7uWSx/cY92R22aWtH5kUsTE07GZYzT78AX1Y/doR0GjRzuDJGc0KY3TUh zag90h4eNDj+oh/z5mP/MLNA4weWVKxZBkXj2u2Xsbul9CT899UX+OztV9t1o9weDwOGjcTl9lBZ tpPaio5rH6Tn5jFt5uEYihkPmDTzSAoGD6WiZDuTZx5J1uBR5GSkMWTMOHx+P00N9axavZqytStY 8lnLtUKlNHj+b3/kxLMvIiUjc5cLVNn4Ayn0HzocoShU7NxBXTtzGsq2b+WTZx7iit/eudv4RBJu /i6RmJZBfX0D91//I6rKWrqQaZlZHDbnDGbPOZ20rBwa62pZtHAh8555uMW2VaUl/OOai7jnpQ/I KCiko8qLuxvCtEdPukqoIrmp7O0hhEBVVdxeL4GcwqT23a/ck11hCwaYb6lIYwNb1rRdBGngoMGE dDM4plmpynZUvHV7EoFmSCJW8Kqw/4B2qyatXLSAo89IvhQ8QEMwyKdPP9zmjeT1+/nO5T/ntB/+ lPT0DHM0QDfYvn4Vbz7yd7547402prHb5SYrLY1GzbwjfTm53PDg8+TlF6AZBrXbt7Bj41oaqquJ pqQwaPhIxh1yOOkDhrF93Qr+fPm5lG1vno1bV13FplXLmTiz4zU/3R4vR552LidfcDGFYyfjcXtQ FZC6zudvvsjDt91AKBhssc/qrz4h1FBLanomHZmcugHZQ0dxzf3PABKvapYaSNQZXcLyBZ/z7tMP ddg/RVUpOuwYAi7Ba889xtb1a1p8P2jkaH7/1GsUDh3RotrblCOP55wfXMrvrvwRaz7/IP75tg3r eOaBe7jq9ns6POaukNIMqKqSdqdD7I5QQz21Zd0XDUUouNxufP4AubupO9OaPiMabWjHfIeu+2N2 rKM9dmtNAOlZ2RTNnM3wsRMYd/B0UvMKcecMor5kE/PmPtNm+xO/dxmX3HArYalQFTWISTPiP2zi wfziH0+R+9c7mPuPO1vsIzCHAVMsuz5YuZPVX3zII+++TvG61RixGDFNQ0oDIRRcbheKqqJ6vEyf fTzpA4e3EA2AlYu/2qVo3PDwC8w4+gQaDJU6zUDTwSPNpSKOP/8Stm3fwdz77mixT3nJDhprayzR aN/ciBgSJT2HcSeehRDmCEOWW8FraXZZdS2v/u02PnjhqQ77VjhsJBfeeDtHnnw6SjTEkvffaLON PyWVVx76a4dtZHhVhGg5SfLT157jgutv63Afm6Jph9B/3EFMmj6DjMLBiLQsQsKNmplL1HpRtV/2 qWNiwToiVd0bPRFCIBQFl8tNIJBCYf/kptrvF0OunW3OVm4pwZOaxpDRRRSvazmXY2d5OT5F4FXB HRPEsIYHWsm+sMbfXUrzAtBVlZXtxhyKphzSYb9SMzI56+qbOebsi3CnZhKWCjqmC5XhVqhdVUFt RdtK5IcePwdFUQlGzFiLJiGmSLy6Qo7Xzdix7btDtnB88tEHPPWbqyjdWoyUbcVTWittARAK8fEb L7fbXqixvsNzA5h68KHUGyqVEbOQUcwAt2rmZahCYcSotiX3q8tLCTbY7XZgaUhruFo38xjcgOEC XdP49L9v8u+7b6Nk07oOXDovx37vR5zz0xtIzR+ArkC4vobVS9pmAq9btoR1y5a0+XxX1NdUs3np /A6/nzL7RL5/420MHDWOsOKhUTfrvIQN8OhWRrBh3XISwkmYHHpDLdHKnbvfcDcoioLb6yUlI4vU rPyk9u0zMQ1o9hVjEnD7yCoY0EY0Frz+HN/76XVkeNLQrOc/YkhcimjhoqjCHFbzqYJ0F2S5Fd58 6Qm0SLjNcfPHti2kYzNt1vGc++NrqdGgPGLQpJuJWR4FYtLAnZqJPzWNpoaWD2e4sdEM6gpzJMcw 7FXqd38dln41n7//7HuEGpKb8twVooakLmpQr5kTzQxrroaCoCEm2/fdpZ1mBR3eCFZsSlfM8/a7 BKVb1/Ofu29nwXuvdziqMnLiVC7+1R8YPfMYQoZCUDPwqoJITHY+yNwJVi1aQFZu+yvd/fxPfyd9 4HAqwga1YUnYShAzyzc0T6o0rGzmJGLJbF/+dbwkQ5cRAtXlwuv1MWrUqKRTbfuce2LYmXteLxOO OJ5vPv2/Ft9X7NjKP+/4DdfceT9g4FIgpIt49qAdUFUVM48gxWUuaLRmzSpefuDuNm+2/kNHMGzo 8A77o7pUmnRBVUSnTjPjI5LmeTPevP6kZ+e0EY23n36QkdMPJycrB1WYiVZeRZCiClYv/ZqXH/57 u8eLRKLMfeS+NoKhqCoTDz2cwWMnUTRlGgC1VVVs2rie6i3rWbHwy3YFcXdE9ObRq5j99rRmHNsJ Yl3BKwQBl4JXSohG+N+zT/PWA3dSXVbarnXhCwQ479pfc/LFPyMlJZXaqEFUNmfTpnhUfIGUFktJ dIdQY32HooHLTXXELDTdGDMtC/veslPRpTUtINm5Jw0rF3Sr3/Ywq8tlxjOGtFPCcnf0GfckrJtT rA0h4pl3E085h8xH7m2xBoqUki9efJxI+VYu+eO/GJjfn/qYJKKbs2jt2IXHqrHhUmDB3Kd4+Pab CLWzXOLR5/yAlPT0jk9TQpMuadLtB8zKBBSg6pCZXcjYGUdRWry5xX7ffDGPG+fM5LCTv0v+4JG4 VahuDLFlyecs+eTDDm/+YF0Vm5d+2ebz71z8E6645U+4/f4W73ZDgh4JUVrTwL9vu5ZP3prbZt9d YafJ2+nVBs2p8Ynp1MniUSFDCL5dtoR/33Urq+fPa3fIWlFUJh02i0tv/Quji8ahqCqaYaaGS8yX qE8VuP0pjJo4hRVftZzDPGHGEfzywRfM3IldENahZP0qho0pQgiB1+fn/Zf+0+62UV0SjpmWV9Sy MuwcILDmCUlzpmsytk9TbRVlG9oG95NFEQoer5eUtHQKh4xMev++457IRPfE9CEzcwo48YrrefFP N7XY1DB0vv7wf6xccghTZ85i5OHHMXL0GLLzC804Qn0t24s3sXb1Kha/+zKlxZvarHcCMGD4KE74 /hVohohPr26NORlNxtPFE4d2I9bb+MhLfsGyzz6kJqGOqDQMyrdt4fVH7kvqMsSiGo31bd2S3H75 ePz+Np8rAhSfn+xAuP2SA7t5DcY9jXg8ScZniMa/7wJaOMQLD9zNa4//i8a6mna3URSVI087i+PP /QF6Uz2rFze/hXXZXLTHBahuF+OOPKGNaKxa+CWP33Y937/uZgaMGtfG/ausrmHxZx/x/tMPsm3D Gu7/7wLyWy1H0ZqoNP/igmoVFmqIyTbTHpKhbvsmQtvWd3FvE2G5Jh6vj7T0TAYNGZZ0G33GPdEx FVyoEiHNilVBXXD4uZdSsWEVH730dKs9JMGaKj5/9zU+f/c1vH4/bisH39B1IuFQu282m6x+BVx5 zyOIlExqogYZ7vZV0FyKwJpkhi1s5jRr3TAtkOwhI7n4d/fy9K3XUlPWvSBXSkYGQ8eMZ+03LddT eenBv9Ko6Uw95FDyBwzC5XZTX13Ntk0bWLNmNV++/hyV7eS1rFv4BcFox9fBtjB29X377PqtEW5q 5PN3X+9QMMCsqP3lf9/kq/ff3mVbACnZudzy2pd8/uoz7NjU/OAZus4nb7zIoo/eI3/wMEZNmERK eiaxaJjN69awZd0amupq47kXLzz0d6669S4UV8emScww4xRx4cRcXqO7RdRKv/4QPdq2qn1S2PEM n58hw4bjCaQl3USfEQ0wU8w9VkBTMyAUk7hcKZx+8z243B7ef/bRDveNhEJtVjrriNz+g7jyvqfJ mnAoNVGJKsxJVe3dFFKaE5gSK2rZZnxMmoHEYEwwctYcfv3sGN546K8seudlwqFdl2Dzp6QSCja2 +EzTY2ioHHP2RW1Eo7GulpfuvZ3XfT48Xh9CmPNTwqHQLgOEqxbO55Hf/pyswvbfrpphJcclnrP1 /3aco31kq393ASmJhjsXh3H5w+RlZ3PDXx/mD1dc0Kb4UrChnk0rl7FpZdt1ehN5/98Pc8y5lzBy 7PgOraioNWnQ/r51GcauoEVClC/5tFttJMYz/IEURo/rOIC/K/pMTAOah+m8ikQXprXRGAPcqZz6 q7sYOW0Gbz/8F7au61pJPV8ghSNOO4/jL7sO74AR1GhmEpJZF6L9EfeYNN2QxDeybW0YCLNeiC5p UgR5Q0fzi7v/RclVN7Lov6+x+puFNFaW01hfixAKaZlZ+PMGMH3mYXjyBvLXK85ucaxQfR1VW9Zz 0nk/oGTjWt555vE2owzRcLjTDxpAXkF/TrngYr754pN2vw/p1rTzVhXApJREpSDawdNiWPVc98gw WntIA6mFGT7tMH792Fzu/+11FC9bmHQzuQMGsWPnTvJGFMXjU60J6xLZKo7RXYLbNlK7Ormh4fZQ hILb4yElLZ38ISO61EbfiWlYhGISl1s0T880JDIGMdXHmFMuoGjWiaz8+F0WvPcGm5Z+SX1N9W7b HDlpGiOnz+KI0y8ga9hYgripjcr41PGYYdaVaO8e0gwzLmAHWe3IvwFmAppVL0KzxGXH9i0QqmfO j67hHLdKTIsQ0zQEApfbjeLx4XG7WPl166KEEKyvo3jDWgaOm8r5N/+ZsUedwrtPPsDGxfNpaieI ayOEYNDI0RQdNJOU/AG899h9hIKN5A8exm+eeo384UVEPv243X2bdMN6QBLyZDAFXNNlhzkItZqk MmoQa7eyPIRjPftekYZOTX2QJsVHRtFB/PKpt1j69kt8+fZLrF26kNguzP6U9AyGH3QYBx87h6Lj TsObkUtt1CDYQZ57SJe4DGlVK+uZ89j08Rto4eQKALdGCIGiqni8PtIzsxg2LPkgKIBYVNq5UNUd 11/O8oXzKS3ZRrgp2G62ZTLMefILskZP7FYbHeG1siLNoVNr5qtViMZv1YVw6Rqx+moqykoIl2yi vqqSzWtXEQ2H6D9sJJk5eWQPGExg4Egys7LAnx6vQxppVSPDrs8QqdyBFx2XFQiMSol0BxAZuaYZ L1umrJuFXMyFgVJdCqmxJh7/+YVsXPIlKVk5jJk4lf6ji/AXDqOfVTpdM6B851a+fOtlNq1uGUn3 eH3c9OgrDJt5LE3W3Sq1KMHqcmq3rKVm53bqqyvYsXE9/YePIiUtg6zCAWQPHEp2fn8CaRkYQmH7 2hUsnvc+U4/7DtlDR6NLiDTWE66vjQ/pa4YpCL68gUSlEl+O0pAynk/iUUCJNqHXV1ml/awaGVKS mleI4vYQrNyJ1KJWYR6r3oXbR2p2Ho1VpRDTUBVQMet3dPZdExcuwwxER6VCILc/UjFjES5hLlmh Rpuor66idv1yKrZvYeO33yAQ9B8+krzCgWQW9CdjyBjc6TnEXB4iloUkBOhNDWgN1bisgkcxKYkY Ek/uQDShWLkZ5j3TkcB0hqbqSv7v56dRv/HbLrcB5rC7P5BCfuFADp11HNffkVyQ3abPWRpgvrGJ SXOhHcOsgCUNuySgpCkmcCtuPOn5ZGXm4xo7FVXAFGF2TFqjHDrmQ1onQYvIeAWo+BCjtCtpgTQk 7twB8TJ7AC5p7qMl1ATd1a2zZN67rPziQ/RYjGBDPeVbN8M7nT/vzIL+ZI6aSLUm41XbFTy4cwaS mzeQgoSKW9CyanuDhLoIgEQdMoGDL56AAVRFTAtJeNIQeelx0VClxGuYwqjZ52ZZU2ZRY7PyldsT wJ2XgqI0lxoEaDTMIQYloyC+BgrW9zGgJgYiszBew1PSXC+0s0jrNxQGKIakUYIRM+L1QV0x8KgB 3HkB+hcMYhBwsF2By7o+ZsUuCFn1XOxYhQAUbyouX2q8VKAKeAyJZpeetK5JuJsBjYoVC2jY3DWX 2qZFfkYghaLx47vcVp8KhCYSsUq1+V0CjyLjC/DoAjTMN4Jir79h7aNYpcDsoVtJc2Vvw2jOQbBv BhsFYQYDdYkmwB5wtN92tsDYYqRZqdaJNFZXMu/xv+1yxGZXeHx+TrniemRGHo2atZyD1UfVeijt QwrRNijbeohUIltsI6z/Sig0Hn8oDdnSf7fbRJqxi5iQCKPVvnZfsK5XoiBY39s1R+0+dwW7oLFd WjHxIBpm3owqBEqsucqWXYELmn9vvZ02BLYIy/g1bXFNsHJVuqEZWriJda8+huzE/KbdEY9npKYx cERRl9vps6IB5o/XGDPL8Jnrg9oFaoU59d2qMZr4BkzEvkHst44tFJJWFoM9hIpZs9O+wZuzAGW8 9J9dMT0FgT1h1t5u3HHfJZCeQcWWDZ1eOCmQlsGoqYcw63tXMOTwk2i0p/vL5n7acYP2zrPNubT+ LuEBV4RZMiDxmtDq/CT2miaWhWLFepSEgjOJ7doParudkHTwRXK07quNWbHdFDVoOyEx8drYmZx2 G4llG+1za32cnqg2XvbNl1SuaJuslyx2PMPr85OZk8uwUWO73FafFg0bXVriASDMmo4C06+NF8ht 9SqTND8tEcMOeMp2hxAVpdW6Hy3asdtouW9USnwJxYXd6Tkc9qNfMvviqxBN9TRUVxIuLaa+vISK Hdso29ZceCWnoD/9Bg8lY8BQcoaPJSU7F80VoDFmCoYdX7Af5myitTUAABOwSURBVMQ+tVjbI+Ec oWURZI9iisCusHe1r4v9LhQ01xUVQrYryPb+e2nspAPpkbgTXbYEywZaCox9D7RusaPf2y5I3B3B 0MJNrH7xn+iRzqUC7BJhBtL9gRQGDBlGWlZy0+ET6VNDrrtDt45rB6USq3LvqkO7Mwx1KzCaDLZY 2DERe39d9ePJCJCRVUDuqAnNJfYR8WrFtqmsWa5OnSHRNBkPtsZdIazU9RY+dcf9jLsVmK5MVw1i 261r2heWdtsNLe8B6Oj69MbKbjsXfkzFN591ux0hBKqi4vF4CaSmMWHi1G611ycDoZ2le+M/3UOX ENXBp5prnUhhBlPtXJPWsRZ7uQawXaWW/rY9z8O2MKLSzBfQuniSB8ryh715D+yKaFMDq56/D6O7 GaC0dE2ysnMZvd+Kxr7/EtrjNOlmLMSrWmP6Vs5Ge7GWuFeR4FHYN3xi7MS2LkL6vr5+vMOuWPPG 01R9m9xKgO1hF9xxezwEUtMYMHgohYO7ltRlc0BbGr2NxBSOiG7GRexV7BXRPHUee4GgVgqQ6G/b Q54RwxzlOVCshL5K+YaVrH/pn8geqv+hKqaVkZGZzYwjj0You5nSuxsOiEDovo65EllzXEOQuNTf 7u2Fnso67CrSkIjOVB522C1aOMTqJ/9MqHxHj7QnFAWXNczar3AA4w86rNtt9t4SBg4dYscmOvvX 225I7ZKPadrSveQjB5M1bz7N9i/e65G27ACo1+sjLSOLKdMPIyOng8JBSeCIhkO3CdVWsvKfv6ah rHO5JQ7tU7J8AWufuRfZAwtEQXPtDH9KKnn5hcw86vgeabdPLJbk0LtUlG5n7fwPWfDXG7s9qepA paFiJ4vv+xXhqo7XtUmGxBGTtIxMJh80nf5DuzZBrTW9JxqOC9xnqCkpBqB8/nusevLPbZaudNg1 WijI8kf/QP3apT3WplAU3G4zlpFfOIBZJ3yn2wFQG8fScOgxpGGw8sUHWf3Wv3u08ndfJqZFWfLE n9nyvxfbLZjcFexYhs8fIDM7l4NnHMHgUV2foNYax9Jw6FEMLcK3D93Kho/fxuhufbs+TiwWY/mL D7LxlYeQu1nKsbPYbonb6yUtI5OBw0ZwzJyzeszKACcQ6rAHiAUbWPbX69j0+XtIRzjaJabrrHz1 cdY9dVePZH3amGu0ukhJTSM3v5CjTphDQRcqju8KRzQc9giR2kqW3n01Gz9717E4WhGLxVj12pOs fvR2YqHG3e/QSWwrw+cPkJGVw+hxEznqhFN7rH0bJ7nLYY8Rqalg6d1Xo0fCjDr29HYXzz7QiEWj LH/5QdY9eVePC4ZQFDwe0y3pP2go3zn3B/hSM3vsGDbO3BOHPUqktpKlf7mWSE0548/4EarH09td 6jW0UJDFj/+ZTXMf6lGXBEzRcLvNgsF5Bf2ZddzJjJkwrUePYePMPXHY48SC9Xz74K3o9dUUnX8V vrSM3u7SXidYWcaSR25n6/9e7LGgp40dx/AHUsjK7ceEyQdx4hnnI9Q983g7Q64OewVDi7Dq3/fw yW2XUbNlXW93Z6+yc8VXfHTTeWx999k9IhiKquLx+UjPymboyDF898If4U/L6tHjJOIMuTrsNaRh ULHgf3x68/ms/+iNPp8EpoVDrHjlEb645SLq1izpsTwMm8Q4RmpaBgX9B3HyaecwbMyeqfJv4wRC HfY6DVs3sOiPP6H6lO8x9oJryOg/pLe71OOUb1jF6ifvZPvn7/a4dWEjhDBXS0tJJadfAYcecTQz jp2zR46ViCMaDr2CHg6y/tVHKVv6KUXfv5FBR5yCLyW1t7vVbaJNDax98z+se+EfhCpK9thxFEVB VV34/AGycvIomjiFU8/9Pm6vb48d08YRDYdepX7zWhbeeSUbp81i6sU38P/t3VtQW/edB/Cv7neJ qyXuRtgoAdtxHBK7Thtir+ok65L1xpa8nSTbyWyh7T51dnjwPhledvzA0+5LB7ZPndnMQtLpDLvT zlbThmyS1okzXTuALRKEwQbjC+YiQHdpH3SxBMLmDxIX5/uZ8YwHcf7nf4TO9/z+5/x1TnHjC5DL d98VlpB/GXe+/Bg33v833L/6WU4eObCW1JfRNBoUFBWjunYf3nqnFUVl1XlbZzpecqVtFw0F8ODy 7/HHa39C+XffgM3xUxTbntsV4RHyL+Petc8x/P6/4v5fPkY0R19rX0vqxKdSBYOxAJaKarz25lnY Dubn8mo2vORKO0bYt4iJ3/dh6pPfovw7p7D/rR+jYN9BaHbgJdql2Qd4MPg5Rj7swYOrnyCS43kX 2aTu96lQQm80YU95JU68/jdo/utzQA6/W/IkrDRoxwn7FjHxh1/j9v/+F0z1h1H7/XOo+s4paCyV 21p9hAI+LN+ZgMf1IcYH+rE4dh2Ibc39zNMDQ2cwomRPGQ6/cBSnHe9syXmMdGKhsdFn42VtK3dN 0dMpGgpiduhzzA59jsFf/gtKDx1DadNJWI58DwZLNVR6Q977sDT/EN7Jcdy/8gdMffER5oYuIxTw 53296VYFhrkMB4+8CMd7P4PWmL/5GGtZd2gkn8wlkUhyGx5E6xD0zmHy099h8tPfQa7RQWdtgGX/ IRgPHofl2cOQ6wyQ64xQabQbXodv0YuIbxHhpQXc/stnWBq+jDsjX8E3fmPb5pRkC4xDR17C2z/5 OSzVuf326nqtOzTkCiVkMtmqxxdu1OKdm5AolDlrj7ZecrJS2LuwpesN+5YwP/QF5oe+AH7zS0gV Sqj2VEJdWoEicznUpZVQmKsgNxVDaSpCUUnmzXQlsRju351GaGEG4fkZhO6Ow3f/Dmbu3EJw5g4C 924jFt3+xyil7o2xIjDe/dk/oXxv/bb1a92hUV1Tg8Ev/wypRBpPP4lkUzPcBv757Q0vS5QuGgrC N+mBb9KD2e3uTI6kLquq1NAbTSjZEx+SvP2Tn29rYAACoVGz7xko1WrIFQpIJVJE+UgeorxITtxS aTQwGAuwp7wSh184Cud7/wjzJp+OlgvrDo2qvXXQG4yQK5SQSKWbrjSIKFPy/IVcrkjc37MYlopq nHj9TfzA8S4023DSM5t1h0Z5TR0MBUVQqdWQyWTI03R6om+l9ElbGp0ehcWlqNpbhzfOOPDKG2e3 /LLq46w7NGQKFWpqrRi9/hVkcnm82ojFWG0QbULy/GByOKI3mFBUasazBw/j7LutqD9wZEsnbq2H 0DyN+obn8KePXFAolPBL+FAcos1Iv5yq0epgKiyGubwSR7/7KlrO/whFlqrt7mJWQqFhqayBVm+A QqmCVCpDVBJhpUEkKFldSGUyKFVq6PQGFJbsQU2dDafPOHHsr05DrlRtdzfXJBQae611KCwqgUar w6J3HmGe2CASkgwLuVyRemRiqaUcB557AX/7Titq6nP3UKN8EQoNlb4QVdZ6TE6Mwbswh3AwiHAs hugOmAhDtJOln7tQqFTQ6Q0wFRajvGovmk+dxmtnzuflzuH5IPyFtdNnf4iJUTcW5mYRCgYQXY4i xhOiRFmlD0UUCiXUGm28ujCXo77xIN48/yPsa3w+p09Ayzfh0Kiorcfrb/0QszP3EfD7EA6F4pVG NMrgIEpIDwu5XAGlSg29wYiCohJUWPfh5KkfoPm1Fii1xu3uqrANfTX++Kun4Ll+DUuLXoRCQUSj UYRCQQYHPVFyZ8r40mNapbqbPz/p2yaTyiBXxMNCq9PDUFAIS3klXjr+Ck6cPgtzlXW7u7thGwoN qUKFM+/+FLcnxuOhEYmkAoTBQWuRSqWpGY8yuRwymQyxWAzRSAThcBjRaCRetSZCZDd8jtKDIjn9 W6FUpq6KGIwFKDFb8PyLx3DijTOosD6zq4Yi2Wz4JjzGwmI4/v7H8P1iCQAgvTeNJe8CgsEAohFe iqVHUtOjExOYNFo9dHoDtDodgsEgAgE//MvLCAb8CIWC8SFvJIJoLJr6tulOqERW3h5CkqgopLJE VaFUQaXWQKPTw1RYhBJzGV48ehxHm0+hvLZ+14dFkuTK9Ob+CiNXL6PvV/+Or4e/wsy9aXgX5hD0 +xGOhFf9wenbI30HS05g0ur0MBUWo8RSju+dPIWXT76Ob4b+D3/+9GPcv3sHcw9nsLy0CP/yEgIB P0LBIMLhECLh+GcpEk0cjLZoOLMyJKRSKaQSKaQyWaJSkkOpSgSFVget3oDCohJUVO/Fy6/a0XDk GAyFpXnr33bZdGgAwMLsDH7zq1/g8icf4e7UbSzMz8LvW06cJM0sOQGGyNMo21FYKpFCJpdDqVRB qzegqNSMfbYGvPl37+HQSy9DmnhsYCQcxG3PCNzXvsTgV1dxd3IiHiCLXvh9ywj4fQgGAxkhkqxE Vn62gI19vtLv67JWSCgUytTQQ6VSQ63RQmcwwlRUjKraOjQefB7PHDoCc5UVEsn2PYcs33ISGgAQ Cwfx2Uf/g99++B+YunUT87MP4VteRDAQSP2hI+FwvORM/pEZHrtfYmdbuYPJ5QrIFfEJTHqDEUWl ZjzXdAxvvf0PKH7CrfZn701h7OvruPXNddwYHsLMg3tYXJjH0qIXvuUl+H2JoUwiRKKRCCKRCGLR KGKIiX2+EgGXDDmJVBo/N5HYBqVKFQ8JtQZanR5avQFGUyFKzWV4trERFdZnUGdrhM5UlIt3c1fI WWgkTY6N4L8/fB8To27Mzc5gybsAv285ccTwIxQMxAMkyvMeTwPJihOACqUKao0Wao02ftXAVIBS SwWOv3ICR0+ehmID06O9D+9hzPMNpm9+jRtD1zB5axze+bn4UCYRINkOTuuZdJgMO5lcDrlCAblc AYVKBbVaA7VGC43eAJOpEHutddjfcAjmKius++qh1u+8O6RvlZyHRlJgcRZjnlFM3xrH18NXMT42 ioW5h1ha9CLg88WPEInSknapxBE6eRJQq9NDbzShrKoGzzYegqWqDrV1VhiLLUAOy3W/dxYTN8cw dXMEN79xw+MZxbJ3IR4ifh+Cfj8ikTAikcjjP18SCWSJSVfJoNDq9NCbCrC/3oZqaz3Ka+tRVVML hVqXs/7vdnkLjZUioQCmxkdxa2wU46M3MH5zDKFg5oNlYrHYqnuGbtfPVtpJfdsp2w/Ej9SWsgpY 99tQWbsf1dZ9UOm29igci0aw+PAupianMHP/LpZm72Jm5gG8CwtYWFpGMBC/oheNhCFJzZ9QwaTX wWgyoai4BNoiC8x7zDCXlUFXaN7S/u82WxYaRPR0eHpP8RJRXjA0iEgIQ4OIhDA0iEgIQ4OIhDA0 iEgIQ4OIhDA0iEgIQ4OIhDA0iEgIQ4OIhDA0iEgIQ4OIhDA0iEgIQ4OIhDA0iEgIQ4OIhDA0iEgI Q4OIhDA0iEgIQ4OIhDA0iEgIQ4OIhDA0iEgIQ4OIhDA0iEgIQ4OIhDA0iEgIQ4OIhDA0iEgIQ4OI hDA0iEgIQ4OIhDA0iEgIQ4OIhDA0iEgIQ4OIhDA0iEgIQ4OIhDA0iEgIQ4OIhDA0iEgIQ4OIhDA0 iEgIQ4OIhDA0iEgIQ4OIhDA0iEgIQ4OIhDA0iEgIQ4OIhDA0iEgIQ4OIhDA0iEgIQ4OIhDA0iEgI Q4OIhDA0iEgIQ4OIhDA0iEgIQ4OIhDA0iEgIQ4OIhDA0iEgIQyNv+nDBIkFTa1/WVz1dB9BkOYBu 90baXbFcvxNNFknqn7NraKOdFufuhHND20G7FUMjrxphHenMskP1oTtXO3a/E02tw2gbiOHKdAxX pgdh7z+wdcFhu4je6UG02bZmdbT9GBp5Zq0HXP0rduD+PrhaHLBvuvUhdHf1wd6TvtM2oq27A+jq hGvT7ROtxtDIM2v7RVgzduAhdHcNo63dkfmL7k4404YYF/qzv+bsGk77+QdwuR2wt6xYqe0c7LZh eNxAcjhzofUAmizOeD8y2uvMGO64Wh/14dHQKjEk6nJmGQI9Gi7Fh1xZls/YtkQfsvVrzffDCReG 0N2ctt5+J5qaO+FZs33KF4ZG3jlgb+mDKxkC7g/gggP2jHK+DxeaO2DtSQwxBjrgaU3uyJmvXUJf 5k5ha4Q163qH4BlJ+7/tP3Fluhf2J7RnT/Zhuhf2/vSh1RC63Y74az0OeLJUMtb2wUfLwoFLPY5E //tgTw6fepA2NEvv18r3Y+UyQNtAbyKA+3ChFbg0cBHWx7ZP+cDQ2AL29g54uuJHRVdXR7z6SP8F 9zA8SKsYbBfR1pLY6d3D8Ng60JZ4zdp+MXMHcw/Fj7arNMJa/+j/9pbGR+t6XHupk6orj9iNj6qj FgfsSFYyq7lanUBPIgjcw/AkqoRk9eFxD6faTPVr1fuRbRkHLvUAFyzrbZ/yQb7dHfhWsJ2DHefh 6gdcIx24tHI4sZl2bR1w9SNziOL+AC53g/jJSXcnnK3ApekY7BhCd/N58T71O3EBvbiSsY0OXMpS TTxetmVy8bu0Waw0tkQj2tob0N3aAbScWz2csDXAivQhTCe6+xOVh60BVncHuhOvZQ4L4kd/V2t6 VRAffmBlBZG+rrXaGxmCJznccX8A10YuB7ciMSzJ3LYnDxnSLiWvuUyi/eleILnN626fcoWVxlZp uYg2G2Btz1KOw4FLAx1wNkvQBABoRNvAYGKnj58baGqNv2Zt74A9/Sjd0osrPfEhRZK9J4beNauZ x7TXchFtXQfgtHQAtpXnXZ4sGV6uZF9sHegduJjYtgNo6krv8+NacmRZpgNtIx3wtA/CjkZY2zvh bO58TPuOtRqnTfp/ll5DOOfC3YEAAAAASUVORK5CYII= "
- id="image4166-8-4"
- x="0"
- y="793.00507"
- style="fill:#008484;fill-opacity:1" />
- </g>
- <g
- style="display:inline;fill:#008484;fill-opacity:1"
- inkscape:label="Trace"
- id="layer2-9-1-52">
- <g
- id="g4216-4-8"
- style="fill:#008484;fill-opacity:1">
- <path
- style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#008484;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:45.99133301;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
- d="M 32.360048,65.606228 A 18.742595,18.742595 0 0 0 13.619017,84.347257 v 52.986143 52.98616 a 18.742595,18.742595 0 0 0 18.741031,18.74102 h 66.931502 52.98614 a 18.742595,18.742595 0 0 0 14.56367,-6.9571 c 9.39466,4.42246 19.71589,6.94877 30.41586,6.9571 a 18.742595,18.742595 0 0 0 0.0133,0 c 39.39268,0.001 71.72926,-32.3342 71.72947,-71.72718 -2.1e-4,-39.392981 -32.33679,-71.728471 -71.72977,-71.727172 a 18.742595,18.742595 0 0 0 -0.0106,0 c -18.56058,0.0092 -35.9395,7.273903 -48.98023,19.491814 C 135.2392,72.880337 117.85926,65.616053 99.29911,65.606228 a 18.742595,18.742595 0 0 0 -0.008,0 z M 32.20937,84.347257 H 99.14088 A 52.987154,52.987154 0 0 1 148.1289,117.2103 52.987154,52.987154 0 0 1 197.12213,84.347257 52.987154,52.987154 0 0 1 250.11087,137.3334 52.987154,52.987154 0 0 1 197.12213,190.31956 52.987154,52.987154 0 0 1 152.12702,165.2527 v 25.06686 H 99.14088 32.20937 V 137.3334 Z"
- id="path4176-3-9"
- inkscape:connector-curvature="0" />
- </g>
- </g>
- </g>
- </g>
-</svg>
diff --git a/src/stylesheets/Paper/Light/backup.svg b/src/stylesheets/Paper/Light/backup.svg
deleted file mode 100644
index 37240d7e..00000000
--- a/src/stylesheets/Paper/Light/backup.svg
+++ /dev/null
@@ -1,126 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
- xmlns:dc="http://purl.org/dc/elements/1.1/"
- xmlns:cc="http://creativecommons.org/ns#"
- xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
- xmlns:svg="http://www.w3.org/2000/svg"
- xmlns="http://www.w3.org/2000/svg"
- xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
- xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
- width="20"
- height="20"
- viewBox="0 0 20 20"
- version="1.1"
- id="svg8"
- inkscape:export-filename="D:\Bethesda Game Modding\Mod Organizer 2\stylesheets\Paper\Dark\dots.png"
- inkscape:export-xdpi="307.20001"
- inkscape:export-ydpi="307.20001"
- inkscape:version="0.92.1 r15371"
- sodipodi:docname="backup.svg">
- <defs
- id="defs2" />
- <sodipodi:namedview
- id="base"
- pagecolor="#505050"
- bordercolor="#666666"
- borderopacity="1.0"
- inkscape:pageopacity="0"
- inkscape:pageshadow="2"
- inkscape:zoom="24.35749"
- inkscape:cx="-2.4145749"
- inkscape:cy="5.3322583"
- inkscape:document-units="px"
- inkscape:current-layer="layer25"
- showgrid="false"
- units="px"
- borderlayer="true"
- inkscape:showpageshadow="false"
- inkscape:snap-page="true"
- inkscape:snap-bbox="true"
- inkscape:bbox-nodes="true"
- inkscape:snap-bbox-edge-midpoints="true"
- inkscape:snap-bbox-midpoints="true"
- showguides="true"
- inkscape:guide-bbox="true"
- inkscape:window-width="1920"
- inkscape:window-height="1027"
- inkscape:window-x="-8"
- inkscape:window-y="-8"
- inkscape:window-maximized="1"
- inkscape:snap-center="true"
- inkscape:snap-object-midpoints="true"
- inkscape:measure-start="18.8975,12.498"
- inkscape:measure-end="20,12.498"
- inkscape:snap-intersection-paths="true"
- inkscape:object-paths="true"
- inkscape:snap-smooth-nodes="true"
- inkscape:snap-midpoints="true"
- inkscape:snap-global="true">
- <sodipodi:guide
- position="10,10"
- orientation="1,0"
- id="guide4039"
- inkscape:locked="false" />
- <sodipodi:guide
- position="10,10"
- orientation="0,1"
- id="guide3935"
- inkscape:locked="false" />
- </sodipodi:namedview>
- <metadata
- id="metadata5">
- <rdf:RDF>
- <cc:Work
- rdf:about="">
- <dc:format>image/svg+xml</dc:format>
- <dc:type
- rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
- <dc:title />
- </cc:Work>
- </rdf:RDF>
- </metadata>
- <g
- inkscape:groupmode="layer"
- id="layer25"
- inkscape:label="Content">
- <g
- id="g3865"
- style="fill:#000000;fill-opacity:1">
- <path
- id="rect3928"
- d="M 0 0 L 0 3 L 0 6 L 3 6 L 3 3 L 17 3 L 17 6 L 20 6 L 20 3 L 20 0 L 0 0 z "
- style="opacity:1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.40999997;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:normal" />
- <g
- id="g3858"
- style="fill:#000000;fill-opacity:1">
- <rect
- y="-12.83"
- x="-20"
- height="5.6599998"
- width="13.981334"
- id="rect3930"
- style="opacity:1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.40999997;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:normal"
- transform="matrix(0,-1,-1,0,0,0)" />
- <path
- sodipodi:type="star"
- style="opacity:1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.83595276;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:normal"
- id="path3932"
- sodipodi:sides="3"
- sodipodi:cx="8.9146461"
- sodipodi:cy="9.684866"
- sodipodi:r1="4.0292788"
- sodipodi:r2="2.0146394"
- sodipodi:arg1="-1.5707963"
- sodipodi:arg2="-0.52359878"
- inkscape:flatsided="true"
- inkscape:rounded="0"
- inkscape:randomized="0"
- d="m 8.9146463,5.6555872 3.4894577,6.0439178 -6.9789157,0 z"
- inkscape:transform-center-y="-1.3333344"
- transform="matrix(-2.1493311,0,0,1.3236446,29.160526,-4.4859875)" />
- </g>
- </g>
- </g>
-</svg>
diff --git a/src/stylesheets/Paper/Light/check-alt.svg b/src/stylesheets/Paper/Light/check-alt.svg
deleted file mode 100644
index c566a383..00000000
--- a/src/stylesheets/Paper/Light/check-alt.svg
+++ /dev/null
@@ -1,94 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
- xmlns:dc="http://purl.org/dc/elements/1.1/"
- xmlns:cc="http://creativecommons.org/ns#"
- xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
- xmlns:svg="http://www.w3.org/2000/svg"
- xmlns="http://www.w3.org/2000/svg"
- xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
- xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
- width="20"
- height="20"
- viewBox="0 0 20 20"
- version="1.1"
- id="svg8"
- inkscape:export-filename="D:\Bethesda Game Modding\Mod Organizer 2\stylesheets\Paper\Dark\dots.png"
- inkscape:export-xdpi="307.20001"
- inkscape:export-ydpi="307.20001"
- inkscape:version="0.92.1 r15371"
- sodipodi:docname="check-alt.svg">
- <defs
- id="defs2" />
- <sodipodi:namedview
- id="base"
- pagecolor="#505050"
- bordercolor="#666666"
- borderopacity="1.0"
- inkscape:pageopacity="0"
- inkscape:pageshadow="2"
- inkscape:zoom="24.357491"
- inkscape:cx="-1.9687918"
- inkscape:cy="8.1433394"
- inkscape:document-units="px"
- inkscape:current-layer="layer25"
- showgrid="false"
- units="px"
- borderlayer="true"
- inkscape:showpageshadow="false"
- inkscape:snap-page="true"
- inkscape:snap-bbox="true"
- inkscape:bbox-nodes="true"
- inkscape:snap-bbox-edge-midpoints="true"
- inkscape:snap-bbox-midpoints="true"
- showguides="true"
- inkscape:guide-bbox="true"
- inkscape:window-width="1920"
- inkscape:window-height="1027"
- inkscape:window-x="-8"
- inkscape:window-y="-8"
- inkscape:window-maximized="1"
- inkscape:snap-center="true"
- inkscape:snap-object-midpoints="true"
- inkscape:measure-start="18.8975,12.498"
- inkscape:measure-end="20,12.498"
- inkscape:snap-intersection-paths="true"
- inkscape:object-paths="true"
- inkscape:snap-smooth-nodes="true"
- inkscape:snap-midpoints="true"
- inkscape:snap-global="true">
- <sodipodi:guide
- position="10,10"
- orientation="1,0"
- id="guide4039"
- inkscape:locked="false" />
- <sodipodi:guide
- position="10,10"
- orientation="0,1"
- id="guide3935"
- inkscape:locked="false" />
- </sodipodi:namedview>
- <metadata
- id="metadata5">
- <rdf:RDF>
- <cc:Work
- rdf:about="">
- <dc:format>image/svg+xml</dc:format>
- <dc:type
- rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
- <dc:title />
- </cc:Work>
- </rdf:RDF>
- </metadata>
- <g
- inkscape:groupmode="layer"
- id="layer25"
- inkscape:label="Content">
- <path
- style="display:inline;opacity:1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:169.70561218;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:1.93939996;stroke-opacity:1;paint-order:markers stroke fill"
- d="m 16,2.0000002 -4,4 -4.0000001,3.9999996 -4,-3.9999996 L -9.4999999e-8,9.9999998 3.9999999,14 l 4,4 L 12,14 16,9.9999998 20,6.0000002 Z"
- id="rect3900-47"
- inkscape:connector-curvature="0" />
- </g>
-</svg>
diff --git a/src/stylesheets/Paper/Light/check-white.svg b/src/stylesheets/Paper/Light/check-white.svg
deleted file mode 100644
index d3f7893d..00000000
--- a/src/stylesheets/Paper/Light/check-white.svg
+++ /dev/null
@@ -1,94 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
- xmlns:dc="http://purl.org/dc/elements/1.1/"
- xmlns:cc="http://creativecommons.org/ns#"
- xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
- xmlns:svg="http://www.w3.org/2000/svg"
- xmlns="http://www.w3.org/2000/svg"
- xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
- xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
- width="20"
- height="20"
- viewBox="0 0 20 20"
- version="1.1"
- id="svg8"
- inkscape:export-filename="D:\Bethesda Game Modding\Mod Organizer 2\stylesheets\Paper\Dark\dots.png"
- inkscape:export-xdpi="307.20001"
- inkscape:export-ydpi="307.20001"
- inkscape:version="0.92.1 r15371"
- sodipodi:docname="check-white.svg">
- <defs
- id="defs2" />
- <sodipodi:namedview
- id="base"
- pagecolor="#505050"
- bordercolor="#666666"
- borderopacity="1.0"
- inkscape:pageopacity="0"
- inkscape:pageshadow="2"
- inkscape:zoom="24.357491"
- inkscape:cx="3.7660651"
- inkscape:cy="2.8283911"
- inkscape:document-units="px"
- inkscape:current-layer="layer25"
- showgrid="false"
- units="px"
- borderlayer="true"
- inkscape:showpageshadow="false"
- inkscape:snap-page="true"
- inkscape:snap-bbox="true"
- inkscape:bbox-nodes="true"
- inkscape:snap-bbox-edge-midpoints="true"
- inkscape:snap-bbox-midpoints="true"
- showguides="true"
- inkscape:guide-bbox="true"
- inkscape:window-width="1920"
- inkscape:window-height="1027"
- inkscape:window-x="-8"
- inkscape:window-y="-8"
- inkscape:window-maximized="1"
- inkscape:snap-center="true"
- inkscape:snap-object-midpoints="true"
- inkscape:measure-start="18.8975,12.498"
- inkscape:measure-end="20,12.498"
- inkscape:snap-intersection-paths="true"
- inkscape:object-paths="true"
- inkscape:snap-smooth-nodes="true"
- inkscape:snap-midpoints="true"
- inkscape:snap-global="true">
- <sodipodi:guide
- position="10,10"
- orientation="1,0"
- id="guide4039"
- inkscape:locked="false" />
- <sodipodi:guide
- position="10,10"
- orientation="0,1"
- id="guide3935"
- inkscape:locked="false" />
- </sodipodi:namedview>
- <metadata
- id="metadata5">
- <rdf:RDF>
- <cc:Work
- rdf:about="">
- <dc:format>image/svg+xml</dc:format>
- <dc:type
- rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
- <dc:title></dc:title>
- </cc:Work>
- </rdf:RDF>
- </metadata>
- <g
- inkscape:groupmode="layer"
- id="layer25"
- inkscape:label="Content">
- <path
- style="display:inline;opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:169.70561218;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:1.93939996;stroke-opacity:1;paint-order:markers stroke fill"
- d="m 16,2.0000002 -4,4 -4.0000001,3.9999996 -4,-3.9999996 L -9.4999999e-8,9.9999998 3.9999999,14 l 4,4 L 12,14 16,9.9999998 20,6.0000002 Z"
- id="rect3900-47"
- inkscape:connector-curvature="0" />
- </g>
-</svg>
diff --git a/src/stylesheets/Paper/Light/check.svg b/src/stylesheets/Paper/Light/check.svg
deleted file mode 100644
index 70701759..00000000
--- a/src/stylesheets/Paper/Light/check.svg
+++ /dev/null
@@ -1,94 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
- xmlns:dc="http://purl.org/dc/elements/1.1/"
- xmlns:cc="http://creativecommons.org/ns#"
- xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
- xmlns:svg="http://www.w3.org/2000/svg"
- xmlns="http://www.w3.org/2000/svg"
- xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
- xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
- width="20"
- height="20"
- viewBox="0 0 20 20"
- version="1.1"
- id="svg8"
- inkscape:export-filename="D:\Bethesda Game Modding\Mod Organizer 2\stylesheets\Paper\Dark\dots.png"
- inkscape:export-xdpi="307.20001"
- inkscape:export-ydpi="307.20001"
- inkscape:version="0.92.1 r15371"
- sodipodi:docname="check.svg">
- <defs
- id="defs2" />
- <sodipodi:namedview
- id="base"
- pagecolor="#505050"
- bordercolor="#666666"
- borderopacity="1.0"
- inkscape:pageopacity="0"
- inkscape:pageshadow="2"
- inkscape:zoom="18.564955"
- inkscape:cx="1.4921607"
- inkscape:cy="9.294465"
- inkscape:document-units="px"
- inkscape:current-layer="layer25"
- showgrid="false"
- units="px"
- borderlayer="true"
- inkscape:showpageshadow="false"
- inkscape:snap-page="true"
- inkscape:snap-bbox="true"
- inkscape:bbox-nodes="true"
- inkscape:snap-bbox-edge-midpoints="true"
- inkscape:snap-bbox-midpoints="true"
- showguides="true"
- inkscape:guide-bbox="true"
- inkscape:window-width="1920"
- inkscape:window-height="1027"
- inkscape:window-x="-8"
- inkscape:window-y="-8"
- inkscape:window-maximized="1"
- inkscape:snap-center="true"
- inkscape:snap-object-midpoints="true"
- inkscape:measure-start="18.8975,12.498"
- inkscape:measure-end="20,12.498"
- inkscape:snap-intersection-paths="true"
- inkscape:object-paths="true"
- inkscape:snap-smooth-nodes="true"
- inkscape:snap-midpoints="true"
- inkscape:snap-global="true">
- <sodipodi:guide
- position="10,10"
- orientation="1,0"
- id="guide4039"
- inkscape:locked="false" />
- <sodipodi:guide
- position="10,10"
- orientation="0,1"
- id="guide3935"
- inkscape:locked="false" />
- </sodipodi:namedview>
- <metadata
- id="metadata5">
- <rdf:RDF>
- <cc:Work
- rdf:about="">
- <dc:format>image/svg+xml</dc:format>
- <dc:type
- rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
- <dc:title></dc:title>
- </cc:Work>
- </rdf:RDF>
- </metadata>
- <g
- inkscape:groupmode="layer"
- id="layer25"
- inkscape:label="Content">
- <path
- style="display:inline;opacity:1;fill:#008f8f;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:169.70561218;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:1.93939996;stroke-opacity:1;paint-order:markers stroke fill"
- d="m 16,2.0000002 -4,4 -4.0000001,3.9999996 -4,-3.9999996 L -9.4999999e-8,9.9999998 3.9999999,14 l 4,4 L 12,14 16,9.9999998 20,6.0000002 Z"
- id="rect3900-47"
- inkscape:connector-curvature="0" />
- </g>
-</svg>
diff --git a/src/stylesheets/Paper/Light/cross.svg b/src/stylesheets/Paper/Light/cross.svg
deleted file mode 100644
index 399525b0..00000000
--- a/src/stylesheets/Paper/Light/cross.svg
+++ /dev/null
@@ -1,94 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
- xmlns:dc="http://purl.org/dc/elements/1.1/"
- xmlns:cc="http://creativecommons.org/ns#"
- xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
- xmlns:svg="http://www.w3.org/2000/svg"
- xmlns="http://www.w3.org/2000/svg"
- xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
- xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
- width="20"
- height="20"
- viewBox="0 0 20 20"
- version="1.1"
- id="svg8"
- inkscape:export-filename="D:\Bethesda Game Modding\Mod Organizer 2\stylesheets\Paper\Dark\dots.png"
- inkscape:export-xdpi="307.20001"
- inkscape:export-ydpi="307.20001"
- inkscape:version="0.92.1 r15371"
- sodipodi:docname="cross.svg">
- <defs
- id="defs2" />
- <sodipodi:namedview
- id="base"
- pagecolor="#505050"
- bordercolor="#666666"
- borderopacity="1.0"
- inkscape:pageopacity="0"
- inkscape:pageshadow="2"
- inkscape:zoom="63.914764"
- inkscape:cx="-8.3166432"
- inkscape:cy="4.7813622"
- inkscape:document-units="px"
- inkscape:current-layer="layer25"
- showgrid="false"
- units="px"
- borderlayer="true"
- inkscape:showpageshadow="false"
- inkscape:snap-page="true"
- inkscape:snap-bbox="true"
- inkscape:bbox-nodes="true"
- inkscape:snap-bbox-edge-midpoints="true"
- inkscape:snap-bbox-midpoints="true"
- showguides="true"
- inkscape:guide-bbox="true"
- inkscape:window-width="1920"
- inkscape:window-height="1027"
- inkscape:window-x="-8"
- inkscape:window-y="-8"
- inkscape:window-maximized="1"
- inkscape:snap-center="true"
- inkscape:snap-object-midpoints="true"
- inkscape:measure-start="18.8975,12.498"
- inkscape:measure-end="20,12.498"
- inkscape:snap-intersection-paths="true"
- inkscape:object-paths="true"
- inkscape:snap-smooth-nodes="true"
- inkscape:snap-midpoints="true"
- inkscape:snap-global="true">
- <sodipodi:guide
- position="10,10"
- orientation="1,0"
- id="guide4039"
- inkscape:locked="false" />
- <sodipodi:guide
- position="10,10"
- orientation="0,1"
- id="guide3935"
- inkscape:locked="false" />
- </sodipodi:namedview>
- <metadata
- id="metadata5">
- <rdf:RDF>
- <cc:Work
- rdf:about="">
- <dc:format>image/svg+xml</dc:format>
- <dc:type
- rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
- <dc:title />
- </cc:Work>
- </rdf:RDF>
- </metadata>
- <g
- inkscape:groupmode="layer"
- id="layer25"
- inkscape:label="Content">
- <path
- style="display:inline;opacity:1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:169.70561218;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:1.93939996;stroke-opacity:1;paint-order:markers stroke fill"
- d="m 4,1.5e-7 -4,3.99999995 2,2 L 6,10 l -4,4 -2,2 4,4 2,-2 4,-4 4,4 2,2 4,-4 -2,-2 -4,-4 4,-3.9999999 2,-2 L 16,1.5e-7 14,2.0000002 10,6.0000001 6,2.0000002 Z"
- id="path3944"
- inkscape:connector-curvature="0" />
- </g>
-</svg>
diff --git a/src/stylesheets/Paper/Light/dots.svg b/src/stylesheets/Paper/Light/dots.svg
deleted file mode 100644
index 02e61276..00000000
--- a/src/stylesheets/Paper/Light/dots.svg
+++ /dev/null
@@ -1,111 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
- xmlns:dc="http://purl.org/dc/elements/1.1/"
- xmlns:cc="http://creativecommons.org/ns#"
- xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
- xmlns:svg="http://www.w3.org/2000/svg"
- xmlns="http://www.w3.org/2000/svg"
- xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
- xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
- width="20"
- height="20"
- viewBox="0 0 20 20"
- version="1.1"
- id="svg8"
- inkscape:export-filename="D:\Bethesda Game Modding\Mod Organizer 2\stylesheets\Paper\Dark\dots.png"
- inkscape:export-xdpi="307.20001"
- inkscape:export-ydpi="307.20001"
- inkscape:version="0.92.1 r15371"
- sodipodi:docname="dots.svg">
- <defs
- id="defs2" />
- <sodipodi:namedview
- id="base"
- pagecolor="#505050"
- bordercolor="#666666"
- borderopacity="1.0"
- inkscape:pageopacity="0"
- inkscape:pageshadow="2"
- inkscape:zoom="17.223346"
- inkscape:cx="-4.9792391"
- inkscape:cy="3.3065594"
- inkscape:document-units="px"
- inkscape:current-layer="layer25"
- showgrid="false"
- units="px"
- borderlayer="true"
- inkscape:showpageshadow="false"
- inkscape:snap-page="true"
- inkscape:snap-bbox="true"
- inkscape:bbox-nodes="true"
- inkscape:snap-bbox-edge-midpoints="true"
- inkscape:snap-bbox-midpoints="true"
- showguides="true"
- inkscape:guide-bbox="true"
- inkscape:window-width="1920"
- inkscape:window-height="1027"
- inkscape:window-x="-8"
- inkscape:window-y="-8"
- inkscape:window-maximized="1"
- inkscape:snap-center="true"
- inkscape:snap-object-midpoints="true"
- inkscape:measure-start="18.8975,12.498"
- inkscape:measure-end="20,12.498"
- inkscape:snap-intersection-paths="true"
- inkscape:object-paths="true"
- inkscape:snap-smooth-nodes="true"
- inkscape:snap-midpoints="true"
- inkscape:snap-global="true">
- <sodipodi:guide
- position="10,10"
- orientation="1,0"
- id="guide4039"
- inkscape:locked="false" />
- <sodipodi:guide
- position="10,10"
- orientation="0,1"
- id="guide3935"
- inkscape:locked="false" />
- </sodipodi:namedview>
- <metadata
- id="metadata5">
- <rdf:RDF>
- <cc:Work
- rdf:about="">
- <dc:format>image/svg+xml</dc:format>
- <dc:type
- rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
- <dc:title />
- </cc:Work>
- </rdf:RDF>
- </metadata>
- <g
- inkscape:groupmode="layer"
- id="layer25"
- inkscape:label="Content">
- <g
- id="g3853"
- style="fill:#000000;fill-opacity:1">
- <circle
- r="2.5"
- cy="10"
- cx="10"
- id="path4063"
- style="opacity:1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:3.99999952;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:stroke fill markers" />
- <circle
- style="opacity:1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:3.99999952;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:stroke fill markers"
- id="circle3937"
- cx="17.5"
- cy="10"
- r="2.5" />
- <circle
- r="2.5"
- cy="10"
- cx="2.5"
- id="circle3939"
- style="opacity:1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:3.99999952;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:stroke fill markers" />
- </g>
- </g>
-</svg>
diff --git a/src/stylesheets/Paper/Light/folder.svg b/src/stylesheets/Paper/Light/folder.svg
deleted file mode 100644
index 888464f7..00000000
--- a/src/stylesheets/Paper/Light/folder.svg
+++ /dev/null
@@ -1,94 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
- xmlns:dc="http://purl.org/dc/elements/1.1/"
- xmlns:cc="http://creativecommons.org/ns#"
- xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
- xmlns:svg="http://www.w3.org/2000/svg"
- xmlns="http://www.w3.org/2000/svg"
- xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
- xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
- width="20"
- height="20"
- viewBox="0 0 20 20"
- version="1.1"
- id="svg8"
- inkscape:export-filename="D:\Bethesda Game Modding\Mod Organizer 2\stylesheets\Paper\Dark\dots.png"
- inkscape:export-xdpi="307.20001"
- inkscape:export-ydpi="307.20001"
- inkscape:version="0.92.1 r15371"
- sodipodi:docname="folder.svg">
- <defs
- id="defs2" />
- <sodipodi:namedview
- id="base"
- pagecolor="#505050"
- bordercolor="#666666"
- borderopacity="1.0"
- inkscape:pageopacity="0"
- inkscape:pageshadow="2"
- inkscape:zoom="68.893388"
- inkscape:cx="-3.0866282"
- inkscape:cy="6.5939937"
- inkscape:document-units="px"
- inkscape:current-layer="layer25"
- showgrid="false"
- units="px"
- borderlayer="true"
- inkscape:showpageshadow="false"
- inkscape:snap-page="true"
- inkscape:snap-bbox="true"
- inkscape:bbox-nodes="true"
- inkscape:snap-bbox-edge-midpoints="true"
- inkscape:snap-bbox-midpoints="true"
- showguides="true"
- inkscape:guide-bbox="true"
- inkscape:window-width="1920"
- inkscape:window-height="1027"
- inkscape:window-x="-8"
- inkscape:window-y="-8"
- inkscape:window-maximized="1"
- inkscape:snap-center="true"
- inkscape:snap-object-midpoints="true"
- inkscape:measure-start="18.8975,12.498"
- inkscape:measure-end="20,12.498"
- inkscape:snap-intersection-paths="true"
- inkscape:object-paths="true"
- inkscape:snap-smooth-nodes="true"
- inkscape:snap-midpoints="true"
- inkscape:snap-global="true">
- <sodipodi:guide
- position="10,10"
- orientation="1,0"
- id="guide4039"
- inkscape:locked="false" />
- <sodipodi:guide
- position="10,10"
- orientation="0,1"
- id="guide3935"
- inkscape:locked="false" />
- </sodipodi:namedview>
- <metadata
- id="metadata5">
- <rdf:RDF>
- <cc:Work
- rdf:about="">
- <dc:format>image/svg+xml</dc:format>
- <dc:type
- rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
- <dc:title />
- </cc:Work>
- </rdf:RDF>
- </metadata>
- <g
- inkscape:groupmode="layer"
- id="layer25"
- inkscape:label="Content">
- <path
- style="display:inline;opacity:1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:normal"
- d="M 12,2.0000002 10,4.0000001 H 2.5e-7 V 18 H 20 V 4.0000001 L 18,2.0000002 h -3 z"
- id="rect3909"
- inkscape:connector-curvature="0" />
- </g>
-</svg>
diff --git a/src/stylesheets/Paper/Light/heart.svg b/src/stylesheets/Paper/Light/heart.svg
deleted file mode 100644
index ffff0a5b..00000000
--- a/src/stylesheets/Paper/Light/heart.svg
+++ /dev/null
@@ -1,111 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
- xmlns:dc="http://purl.org/dc/elements/1.1/"
- xmlns:cc="http://creativecommons.org/ns#"
- xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
- xmlns:svg="http://www.w3.org/2000/svg"
- xmlns="http://www.w3.org/2000/svg"
- xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
- xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
- width="20"
- height="20"
- viewBox="0 0 20 20"
- version="1.1"
- id="svg8"
- inkscape:export-filename="D:\Bethesda Game Modding\Mod Organizer 2\stylesheets\Paper\Dark\dots.png"
- inkscape:export-xdpi="307.20001"
- inkscape:export-ydpi="307.20001"
- inkscape:version="0.92.1 r15371"
- sodipodi:docname="heart.svg">
- <defs
- id="defs2" />
- <sodipodi:namedview
- id="base"
- pagecolor="#505050"
- bordercolor="#666666"
- borderopacity="1.0"
- inkscape:pageopacity="0"
- inkscape:pageshadow="2"
- inkscape:zoom="63.914764"
- inkscape:cx="-5.6913454"
- inkscape:cy="10.121962"
- inkscape:document-units="px"
- inkscape:current-layer="layer25"
- showgrid="false"
- units="px"
- borderlayer="true"
- inkscape:showpageshadow="false"
- inkscape:snap-page="true"
- inkscape:snap-bbox="true"
- inkscape:bbox-nodes="true"
- inkscape:snap-bbox-edge-midpoints="true"
- inkscape:snap-bbox-midpoints="true"
- showguides="true"
- inkscape:guide-bbox="true"
- inkscape:window-width="1920"
- inkscape:window-height="1027"
- inkscape:window-x="-8"
- inkscape:window-y="-8"
- inkscape:window-maximized="1"
- inkscape:snap-center="true"
- inkscape:snap-object-midpoints="true"
- inkscape:measure-start="18.8975,12.498"
- inkscape:measure-end="20,12.498"
- inkscape:snap-intersection-paths="true"
- inkscape:object-paths="true"
- inkscape:snap-smooth-nodes="true"
- inkscape:snap-midpoints="true"
- inkscape:snap-global="true">
- <sodipodi:guide
- position="10,10"
- orientation="1,0"
- id="guide4039"
- inkscape:locked="false" />
- <sodipodi:guide
- position="10,10"
- orientation="0,1"
- id="guide3935"
- inkscape:locked="false" />
- </sodipodi:namedview>
- <metadata
- id="metadata5">
- <rdf:RDF>
- <cc:Work
- rdf:about="">
- <dc:format>image/svg+xml</dc:format>
- <dc:type
- rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
- <dc:title />
- </cc:Work>
- </rdf:RDF>
- </metadata>
- <g
- inkscape:groupmode="layer"
- id="layer25"
- inkscape:label="Content">
- <g
- style="display:inline;fill:#000000;fill-opacity:1"
- id="g4198"
- transform="translate(0,1)">
- <circle
- r="5.5"
- cy="5.5"
- cx="5.5"
- id="path4174"
- style="opacity:1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:5.99999952;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:normal" />
- <circle
- r="5.5"
- style="opacity:1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:6;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:normal"
- id="circle4176"
- cx="14.5"
- cy="5.5" />
- <path
- id="path4178"
- d="M 0,5.5 C 0,10.950465 5.8158604,15.070234 10,18 14.18414,15.070234 20,10.950465 20,5.5 H 10 Z"
- style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.99999994px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
- inkscape:connector-curvature="0" />
- </g>
- </g>
-</svg>
diff --git a/src/stylesheets/Paper/Light/refresh.svg b/src/stylesheets/Paper/Light/refresh.svg
deleted file mode 100644
index 5a1ee108..00000000
--- a/src/stylesheets/Paper/Light/refresh.svg
+++ /dev/null
@@ -1,103 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
- xmlns:dc="http://purl.org/dc/elements/1.1/"
- xmlns:cc="http://creativecommons.org/ns#"
- xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
- xmlns:svg="http://www.w3.org/2000/svg"
- xmlns="http://www.w3.org/2000/svg"
- xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
- xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
- width="20"
- height="20"
- viewBox="0 0 20 20"
- version="1.1"
- id="svg8"
- inkscape:export-filename="D:\Bethesda Game Modding\Mod Organizer 2\stylesheets\Paper\Dark\dots.png"
- inkscape:export-xdpi="307.20001"
- inkscape:export-ydpi="307.20001"
- inkscape:version="0.92.1 r15371"
- sodipodi:docname="refresh.svg">
- <defs
- id="defs2" />
- <sodipodi:namedview
- id="base"
- pagecolor="#505050"
- bordercolor="#666666"
- borderopacity="1.0"
- inkscape:pageopacity="0"
- inkscape:pageshadow="2"
- inkscape:zoom="24.357491"
- inkscape:cx="-2.3896587"
- inkscape:cy="9.2135439"
- inkscape:document-units="px"
- inkscape:current-layer="layer25"
- showgrid="false"
- units="px"
- borderlayer="true"
- inkscape:showpageshadow="false"
- inkscape:snap-page="true"
- inkscape:snap-bbox="true"
- inkscape:bbox-nodes="true"
- inkscape:snap-bbox-edge-midpoints="true"
- inkscape:snap-bbox-midpoints="true"
- showguides="true"
- inkscape:guide-bbox="true"
- inkscape:window-width="1920"
- inkscape:window-height="1027"
- inkscape:window-x="-8"
- inkscape:window-y="-8"
- inkscape:window-maximized="1"
- inkscape:snap-center="true"
- inkscape:snap-object-midpoints="true"
- inkscape:measure-start="18.8975,12.498"
- inkscape:measure-end="20,12.498"
- inkscape:snap-intersection-paths="true"
- inkscape:object-paths="true"
- inkscape:snap-smooth-nodes="true"
- inkscape:snap-midpoints="true"
- inkscape:snap-global="true">
- <sodipodi:guide
- position="10,10"
- orientation="1,0"
- id="guide4039"
- inkscape:locked="false" />
- <sodipodi:guide
- position="10,10"
- orientation="0,1"
- id="guide3935"
- inkscape:locked="false" />
- </sodipodi:namedview>
- <metadata
- id="metadata5">
- <rdf:RDF>
- <cc:Work
- rdf:about="">
- <dc:format>image/svg+xml</dc:format>
- <dc:type
- rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
- <dc:title />
- </cc:Work>
- </rdf:RDF>
- </metadata>
- <g
- inkscape:groupmode="layer"
- id="layer25"
- inkscape:label="Content">
- <g
- style="display:inline;fill:#000000;fill-opacity:1"
- id="g3986">
- <path
- inkscape:connector-curvature="0"
- id="path4137"
- d="M 0,11.998047 V 20 L 2.9335938,17.066406 A 10,10 0 0 0 10,20 10,10 0 0 0 19.787109,11.998047 H 16.705078 A 7,7 0 0 1 10,17 7,7 0 0 1 5.0527344,14.947266 L 8,11.998047 Z"
- style="opacity:1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:6;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:normal" />
- <path
- inkscape:connector-curvature="0"
- id="rect4142"
- d="M 10,0 A 10,10 0 0 0 0.21289062,8.0019531 H 3.2949219 A 7,7 0 0 1 10,3 7,7 0 0 1 14.947266,5.0527344 L 12,8.0019531 h 8 V 0 L 17.066406,2.9335938 A 10,10 0 0 0 10,0 Z"
- style="opacity:1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:6;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:normal" />
- </g>
- </g>
-</svg>
diff --git a/src/stylesheets/Paper/Light/restore-alt.svg b/src/stylesheets/Paper/Light/restore-alt.svg
deleted file mode 100644
index 9f53e511..00000000
--- a/src/stylesheets/Paper/Light/restore-alt.svg
+++ /dev/null
@@ -1,126 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
- xmlns:dc="http://purl.org/dc/elements/1.1/"
- xmlns:cc="http://creativecommons.org/ns#"
- xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
- xmlns:svg="http://www.w3.org/2000/svg"
- xmlns="http://www.w3.org/2000/svg"
- xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
- xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
- width="20"
- height="20"
- viewBox="0 0 20 20"
- version="1.1"
- id="svg8"
- inkscape:export-filename="D:\Bethesda Game Modding\Mod Organizer 2\stylesheets\Paper\Dark\dots.png"
- inkscape:export-xdpi="307.20001"
- inkscape:export-ydpi="307.20001"
- inkscape:version="0.92.1 r15371"
- sodipodi:docname="restore.svg">
- <defs
- id="defs2" />
- <sodipodi:namedview
- id="base"
- pagecolor="#505050"
- bordercolor="#666666"
- borderopacity="1.0"
- inkscape:pageopacity="0"
- inkscape:pageshadow="2"
- inkscape:zoom="24.35749"
- inkscape:cx="16.864936"
- inkscape:cy="2.6868089"
- inkscape:document-units="px"
- inkscape:current-layer="layer25"
- showgrid="false"
- units="px"
- borderlayer="true"
- inkscape:showpageshadow="false"
- inkscape:snap-page="true"
- inkscape:snap-bbox="true"
- inkscape:bbox-nodes="true"
- inkscape:snap-bbox-edge-midpoints="true"
- inkscape:snap-bbox-midpoints="true"
- showguides="true"
- inkscape:guide-bbox="true"
- inkscape:window-width="1920"
- inkscape:window-height="1027"
- inkscape:window-x="-8"
- inkscape:window-y="-8"
- inkscape:window-maximized="1"
- inkscape:snap-center="true"
- inkscape:snap-object-midpoints="true"
- inkscape:measure-start="18.8975,12.498"
- inkscape:measure-end="20,12.498"
- inkscape:snap-intersection-paths="true"
- inkscape:object-paths="true"
- inkscape:snap-smooth-nodes="true"
- inkscape:snap-midpoints="true"
- inkscape:snap-global="true">
- <sodipodi:guide
- position="10,10"
- orientation="1,0"
- id="guide4039"
- inkscape:locked="false" />
- <sodipodi:guide
- position="10,10"
- orientation="0,1"
- id="guide3935"
- inkscape:locked="false" />
- </sodipodi:namedview>
- <metadata
- id="metadata5">
- <rdf:RDF>
- <cc:Work
- rdf:about="">
- <dc:format>image/svg+xml</dc:format>
- <dc:type
- rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
- <dc:title />
- </cc:Work>
- </rdf:RDF>
- </metadata>
- <g
- inkscape:groupmode="layer"
- id="layer25"
- inkscape:label="Content">
- <g
- id="g3866"
- style="fill:#000000;fill-opacity:1;stroke:none">
- <path
- id="rect3928"
- d="M 0 14 L 0 17 L 0 20 L 20 20 L 20 17 L 20 14 L 17 14 L 17 17 L 3 17 L 3 14 L 0 14 z "
- style="opacity:1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.40999997;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:normal" />
- <g
- id="g3860"
- style="fill:#000000;fill-opacity:1;stroke:none">
- <rect
- y="-12.83"
- x="7.85585e-016"
- height="5.6599998"
- width="13.981334"
- id="rect3930"
- style="opacity:1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.40999997;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:normal"
- transform="rotate(90)" />
- <path
- sodipodi:type="star"
- style="opacity:1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.83595276;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:normal"
- id="path3932"
- sodipodi:sides="3"
- sodipodi:cx="8.9146461"
- sodipodi:cy="9.684866"
- sodipodi:r1="4.0292788"
- sodipodi:r2="2.0146394"
- sodipodi:arg1="-1.5707963"
- sodipodi:arg2="-0.52359878"
- inkscape:flatsided="true"
- inkscape:rounded="0"
- inkscape:randomized="0"
- d="m 8.9146463,5.6555872 3.4894577,6.0439178 -6.9789157,0 z"
- inkscape:transform-center-y="-1.3333344"
- transform="matrix(-2.1493311,0,0,-1.3236446,29.160526,24.485987)" />
- </g>
- </g>
- </g>
-</svg>
diff --git a/src/stylesheets/Paper/Light/restore.svg b/src/stylesheets/Paper/Light/restore.svg
deleted file mode 100644
index 33aea044..00000000
--- a/src/stylesheets/Paper/Light/restore.svg
+++ /dev/null
@@ -1,94 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
- xmlns:dc="http://purl.org/dc/elements/1.1/"
- xmlns:cc="http://creativecommons.org/ns#"
- xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
- xmlns:svg="http://www.w3.org/2000/svg"
- xmlns="http://www.w3.org/2000/svg"
- xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
- xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
- width="20"
- height="20"
- viewBox="0 0 20 20"
- version="1.1"
- id="svg8"
- inkscape:export-filename="D:\Bethesda Game Modding\Mod Organizer 2\stylesheets\Paper\Dark\dots.png"
- inkscape:export-xdpi="307.20001"
- inkscape:export-ydpi="307.20001"
- inkscape:version="0.92.1 r15371"
- sodipodi:docname="restore.svg">
- <defs
- id="defs2" />
- <sodipodi:namedview
- id="base"
- pagecolor="#505050"
- bordercolor="#666666"
- borderopacity="1.0"
- inkscape:pageopacity="0"
- inkscape:pageshadow="2"
- inkscape:zoom="29.64796"
- inkscape:cx="-4.0321502"
- inkscape:cy="7.0312982"
- inkscape:document-units="px"
- inkscape:current-layer="layer25"
- showgrid="false"
- units="px"
- borderlayer="true"
- inkscape:showpageshadow="false"
- inkscape:snap-page="true"
- inkscape:snap-bbox="true"
- inkscape:bbox-nodes="true"
- inkscape:snap-bbox-edge-midpoints="true"
- inkscape:snap-bbox-midpoints="true"
- showguides="true"
- inkscape:guide-bbox="true"
- inkscape:window-width="1920"
- inkscape:window-height="1027"
- inkscape:window-x="-8"
- inkscape:window-y="-8"
- inkscape:window-maximized="1"
- inkscape:snap-center="true"
- inkscape:snap-object-midpoints="true"
- inkscape:measure-start="18.8975,12.498"
- inkscape:measure-end="20,12.498"
- inkscape:snap-intersection-paths="true"
- inkscape:object-paths="true"
- inkscape:snap-smooth-nodes="true"
- inkscape:snap-midpoints="true"
- inkscape:snap-global="true">
- <sodipodi:guide
- position="10,10"
- orientation="1,0"
- id="guide4039"
- inkscape:locked="false" />
- <sodipodi:guide
- position="10,10"
- orientation="0,1"
- id="guide3935"
- inkscape:locked="false" />
- </sodipodi:namedview>
- <metadata
- id="metadata5">
- <rdf:RDF>
- <cc:Work
- rdf:about="">
- <dc:format>image/svg+xml</dc:format>
- <dc:type
- rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
- <dc:title />
- </cc:Work>
- </rdf:RDF>
- </metadata>
- <g
- inkscape:groupmode="layer"
- id="layer25"
- inkscape:label="Content">
- <path
- style="opacity:1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:3.00000024;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:stroke fill markers"
- d="m 10,-2.5e-7 a 10,10 0 0 1 0.998047,0.05078 A 10,10 0 0 1 19.787109,8.0019518 10,10 0 0 1 20,10 10,10 0 0 1 10,20 10,10 0 0 1 0.2128895,11.998047 h 3.082032 A 7,7 0 0 0 10,17 a 7,7 0 0 0 7,-7 7,7 0 0 0 -0.294922,-1.9980473 7,7 0 0 0 -0.53125,-1.291016 7,7 0 0 0 0,-0.0019 7,7 0 0 0 -0.359375,-0.599609 7,7 0 0 0 -0.417969,-0.5625 7,7 0 0 0 -0.40625,-0.447266 7,7 0 0 0 -0.07422,-0.07813 7,7 0 0 0 -0.435547,-0.390625 7,7 0 0 0 -0.08594,-0.07422 7,7 0 0 0 -0.490234,-0.359375 7,7 0 0 0 -0.07227,-0.05078 7,7 0 0 0 -0.49023,-0.289062 7,7 0 0 0 -0.119141,-0.06836 7,7 0 0 0 -0.439453,-0.203125 7,7 0 0 0 -0.207031,-0.0918 7,7 0 0 0 -0.402344,-0.138672 7,7 0 0 0 -0.259766,-0.08594 7,7 0 0 0 -0.466797,-0.109375 7,7 0 0 0 -0.208984,-0.04687 7,7 0 0 0 -0.458984,-0.06055 7,7 0 0 0 -0.246094,-0.03125 A 7,7 0 0 0 10,2.9999997 a 7,7 0 0 0 -2.06836,0.314453 7,7 0 0 0 -0.0019,0 7,7 0 0 0 -0.654297,0.240234 7,7 0 0 0 -0.0019,0 7,7 0 0 0 -0.628906,0.304688 7,7 0 0 0 -1.587891,1.197266 L 8,8.0019527 H 3.2968745 0.2031245 -5e-7 V -2.5e-7 l 2.933594,2.93359395 a 10,10 0 0 1 0.01172,-0.01172 10,10 0 0 1 0.591797,-0.535156 10,10 0 0 1 0.169922,-0.148438 10,10 0 0 1 0.6015635,-0.447265 10,10 0 0 1 0.199218,-0.142579 10,10 0 0 1 0.644532,-0.380859 10,10 0 0 1 0.222656,-0.126953 10,10 0 0 1 0.632812,-0.29492195 10,10 0 0 1 0.283204,-0.126953 10,10 0 0 1 0.623046,-0.216797 10,10 0 0 1 0.328125,-0.109375 10,10 0 0 1 0.601563,-0.144531 10,10 0 0 1 0.375,-0.08398 10,10 0 0 1 0.603516,-0.08203 10,10 0 0 1 0.394531,-0.04883 A 10,10 0 0 1 10,-2.5e-7 Z"
- id="path3956"
- inkscape:connector-curvature="0" />
- </g>
-</svg>
diff --git a/src/stylesheets/Paper/Light/run.svg b/src/stylesheets/Paper/Light/run.svg
deleted file mode 100644
index 008b2ccc..00000000
--- a/src/stylesheets/Paper/Light/run.svg
+++ /dev/null
@@ -1,109 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
- xmlns:dc="http://purl.org/dc/elements/1.1/"
- xmlns:cc="http://creativecommons.org/ns#"
- xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
- xmlns:svg="http://www.w3.org/2000/svg"
- xmlns="http://www.w3.org/2000/svg"
- xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
- xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
- width="128"
- height="128"
- viewBox="0 0 128 128"
- version="1.1"
- id="svg8"
- inkscape:export-filename="D:\Bethesda Game Modding\Mod Organizer 2\stylesheets\Paper\Dark\dots.png"
- inkscape:export-xdpi="307.20001"
- inkscape:export-ydpi="307.20001"
- inkscape:version="0.92.1 r15371"
- sodipodi:docname="run.svg">
- <defs
- id="defs2" />
- <sodipodi:namedview
- id="base"
- pagecolor="#505050"
- bordercolor="#666666"
- borderopacity="1.0"
- inkscape:pageopacity="0"
- inkscape:pageshadow="2"
- inkscape:zoom="3.0446864"
- inkscape:cx="23.34915"
- inkscape:cy="69.793752"
- inkscape:document-units="px"
- inkscape:current-layer="layer25"
- showgrid="false"
- units="px"
- borderlayer="true"
- inkscape:showpageshadow="false"
- inkscape:snap-page="true"
- inkscape:snap-bbox="true"
- inkscape:bbox-nodes="true"
- inkscape:snap-bbox-edge-midpoints="true"
- inkscape:snap-bbox-midpoints="true"
- showguides="true"
- inkscape:guide-bbox="true"
- inkscape:window-width="1920"
- inkscape:window-height="1027"
- inkscape:window-x="-8"
- inkscape:window-y="-8"
- inkscape:window-maximized="1"
- inkscape:snap-center="true"
- inkscape:snap-object-midpoints="true"
- inkscape:measure-start="18.8975,12.498"
- inkscape:measure-end="20,12.498"
- inkscape:snap-intersection-paths="true"
- inkscape:object-paths="true"
- inkscape:snap-smooth-nodes="true"
- inkscape:snap-midpoints="true"
- inkscape:snap-global="true">
- <sodipodi:guide
- position="64,64"
- orientation="1,0"
- id="guide4039"
- inkscape:locked="false" />
- <sodipodi:guide
- position="64,64"
- orientation="0,1"
- id="guide3935"
- inkscape:locked="false"
- inkscape:label=""
- inkscape:color="rgb(0,0,255)" />
- </sodipodi:namedview>
- <metadata
- id="metadata5">
- <rdf:RDF>
- <cc:Work
- rdf:about="">
- <dc:format>image/svg+xml</dc:format>
- <dc:type
- rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
- <dc:title></dc:title>
- </cc:Work>
- </rdf:RDF>
- </metadata>
- <g
- inkscape:groupmode="layer"
- id="layer25"
- inkscape:label="Content"
- transform="translate(0,108)">
- <path
- sodipodi:type="star"
- style="display:inline;opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.99999994;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:normal"
- id="path3868"
- sodipodi:sides="3"
- sodipodi:cx="63.999996"
- sodipodi:cy="-43.999996"
- sodipodi:r1="64.000008"
- sodipodi:r2="32.000004"
- sodipodi:arg1="0"
- sodipodi:arg2="1.0471976"
- inkscape:flatsided="true"
- inkscape:rounded="0"
- inkscape:randomized="0"
- d="m 128,-43.999996 -96.000008,55.425632 0,-110.851265 z"
- inkscape:transform-center-x="-15.999997"
- inkscape:transform-center-y="3.8146973e-006" />
- </g>
-</svg>
diff --git a/src/stylesheets/Paper/Light/shortcut.svg b/src/stylesheets/Paper/Light/shortcut.svg
deleted file mode 100644
index 45c9a748..00000000
--- a/src/stylesheets/Paper/Light/shortcut.svg
+++ /dev/null
@@ -1,94 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
- xmlns:dc="http://purl.org/dc/elements/1.1/"
- xmlns:cc="http://creativecommons.org/ns#"
- xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
- xmlns:svg="http://www.w3.org/2000/svg"
- xmlns="http://www.w3.org/2000/svg"
- xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
- xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
- width="20"
- height="20"
- viewBox="0 0 20 20"
- version="1.1"
- id="svg8"
- inkscape:export-filename="D:\Bethesda Game Modding\Mod Organizer 2\stylesheets\Paper\Dark\dots.png"
- inkscape:export-xdpi="307.20001"
- inkscape:export-ydpi="307.20001"
- inkscape:version="0.92.1 r15371"
- sodipodi:docname="shortcut.svg">
- <defs
- id="defs2" />
- <sodipodi:namedview
- id="base"
- pagecolor="#505050"
- bordercolor="#666666"
- borderopacity="1.0"
- inkscape:pageopacity="0"
- inkscape:pageshadow="2"
- inkscape:zoom="22.59728"
- inkscape:cx="-2.8635478"
- inkscape:cy="2.7808125"
- inkscape:document-units="px"
- inkscape:current-layer="layer25"
- showgrid="false"
- units="px"
- borderlayer="true"
- inkscape:showpageshadow="false"
- inkscape:snap-page="true"
- inkscape:snap-bbox="true"
- inkscape:bbox-nodes="true"
- inkscape:snap-bbox-edge-midpoints="true"
- inkscape:snap-bbox-midpoints="true"
- showguides="true"
- inkscape:guide-bbox="true"
- inkscape:window-width="1920"
- inkscape:window-height="1027"
- inkscape:window-x="-8"
- inkscape:window-y="-8"
- inkscape:window-maximized="1"
- inkscape:snap-center="true"
- inkscape:snap-object-midpoints="true"
- inkscape:measure-start="18.8975,12.498"
- inkscape:measure-end="20,12.498"
- inkscape:snap-intersection-paths="true"
- inkscape:object-paths="true"
- inkscape:snap-smooth-nodes="true"
- inkscape:snap-midpoints="true"
- inkscape:snap-global="true">
- <sodipodi:guide
- position="10,10"
- orientation="1,0"
- id="guide4039"
- inkscape:locked="false" />
- <sodipodi:guide
- position="10,10"
- orientation="0,1"
- id="guide3935"
- inkscape:locked="false" />
- </sodipodi:namedview>
- <metadata
- id="metadata5">
- <rdf:RDF>
- <cc:Work
- rdf:about="">
- <dc:format>image/svg+xml</dc:format>
- <dc:type
- rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
- <dc:title></dc:title>
- </cc:Work>
- </rdf:RDF>
- </metadata>
- <g
- inkscape:groupmode="layer"
- id="layer25"
- inkscape:label="Content">
- <path
- inkscape:connector-curvature="0"
- style="display:inline;opacity:1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:3;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:stroke fill markers"
- d="M 12,1.1035163 V 5.4492195 A 9.7951674,7.8360994 0 0 0 2.2050782,13.285156 9.7951674,7.8360994 0 0 0 6.9863282,20 H 8.201172 A 5.8526129,4.68207 0 0 1 6.1484376,16.4375 5.8526129,4.68207 0 0 1 12,11.757812 v 4.345704 l 8,-7.5 z"
- id="rect3929" />
- </g>
-</svg>
diff --git a/src/stylesheets/Paper/Light/sort.svg b/src/stylesheets/Paper/Light/sort.svg
deleted file mode 100644
index 94b5a4f1..00000000
--- a/src/stylesheets/Paper/Light/sort.svg
+++ /dev/null
@@ -1,125 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
- xmlns:dc="http://purl.org/dc/elements/1.1/"
- xmlns:cc="http://creativecommons.org/ns#"
- xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
- xmlns:svg="http://www.w3.org/2000/svg"
- xmlns="http://www.w3.org/2000/svg"
- xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
- xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
- width="20"
- height="20"
- viewBox="0 0 20 20"
- version="1.1"
- id="svg8"
- inkscape:export-filename="D:\Bethesda Game Modding\Mod Organizer 2\stylesheets\Paper\Dark\dots.png"
- inkscape:export-xdpi="307.20001"
- inkscape:export-ydpi="307.20001"
- inkscape:version="0.92.1 r15371"
- sodipodi:docname="sort.svg">
- <defs
- id="defs2" />
- <sodipodi:namedview
- id="base"
- pagecolor="#505050"
- bordercolor="#666666"
- borderopacity="1.0"
- inkscape:pageopacity="0"
- inkscape:pageshadow="2"
- inkscape:zoom="17.223346"
- inkscape:cx="2.707289"
- inkscape:cy="1.8950951"
- inkscape:document-units="px"
- inkscape:current-layer="layer25"
- showgrid="false"
- units="px"
- borderlayer="true"
- inkscape:showpageshadow="false"
- inkscape:snap-page="true"
- inkscape:snap-bbox="true"
- inkscape:bbox-nodes="true"
- inkscape:snap-bbox-edge-midpoints="true"
- inkscape:snap-bbox-midpoints="true"
- showguides="true"
- inkscape:guide-bbox="true"
- inkscape:window-width="1920"
- inkscape:window-height="1027"
- inkscape:window-x="-8"
- inkscape:window-y="-8"
- inkscape:window-maximized="1"
- inkscape:snap-center="true"
- inkscape:snap-object-midpoints="true"
- inkscape:measure-start="18.8975,12.498"
- inkscape:measure-end="20,12.498"
- inkscape:snap-intersection-paths="true"
- inkscape:object-paths="true"
- inkscape:snap-smooth-nodes="true"
- inkscape:snap-midpoints="true"
- inkscape:snap-global="true">
- <sodipodi:guide
- position="10,10"
- orientation="1,0"
- id="guide4039"
- inkscape:locked="false" />
- <sodipodi:guide
- position="10,10"
- orientation="0,1"
- id="guide3935"
- inkscape:locked="false" />
- </sodipodi:namedview>
- <metadata
- id="metadata5">
- <rdf:RDF>
- <cc:Work
- rdf:about="">
- <dc:format>image/svg+xml</dc:format>
- <dc:type
- rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
- <dc:title />
- </cc:Work>
- </rdf:RDF>
- </metadata>
- <g
- inkscape:groupmode="layer"
- id="layer25"
- inkscape:label="Content">
- <g
- id="g3898"
- style="fill:#000000;fill-opacity:1">
- <g
- id="g3886"
- style="fill:#000000;fill-opacity:1">
- <rect
- y="4"
- x="13"
- height="16"
- width="3"
- id="rect4003"
- style="display:inline;opacity:1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.40999997;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:normal" />
- <path
- style="display:inline;opacity:1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.40999997;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:normal"
- d="m 13,0 v 7.9999998 h 7 z"
- id="path4009"
- inkscape:connector-curvature="0" />
- </g>
- <g
- id="g3890"
- style="fill:#000000;fill-opacity:1">
- <rect
- style="display:inline;opacity:1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.40999997;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:normal"
- id="rect3999"
- width="3"
- height="16"
- x="4"
- y="2.2775268e-008" />
- <path
- inkscape:connector-curvature="0"
- id="path4016"
- d="M 7,20 V 12 H 0 Z"
- style="display:inline;opacity:1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.40999997;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:120;stroke-opacity:1;paint-order:normal" />
- </g>
- </g>
- </g>
-</svg>
diff --git a/src/stylesheets/Paper/Light/unchecked-alt.svg b/src/stylesheets/Paper/Light/unchecked-alt.svg
deleted file mode 100644
index ae3c96b4..00000000
--- a/src/stylesheets/Paper/Light/unchecked-alt.svg
+++ /dev/null
@@ -1,94 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
- xmlns:dc="http://purl.org/dc/elements/1.1/"
- xmlns:cc="http://creativecommons.org/ns#"
- xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
- xmlns:svg="http://www.w3.org/2000/svg"
- xmlns="http://www.w3.org/2000/svg"
- xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
- xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
- width="20"
- height="20"
- viewBox="0 0 20 20"
- version="1.1"
- id="svg8"
- inkscape:export-filename="D:\Bethesda Game Modding\Mod Organizer 2\stylesheets\Paper\Dark\dots.png"
- inkscape:export-xdpi="307.20001"
- inkscape:export-ydpi="307.20001"
- inkscape:version="0.92.1 r15371"
- sodipodi:docname="unchecked-alt.svg">
- <defs
- id="defs2" />
- <sodipodi:namedview
- id="base"
- pagecolor="#505050"
- bordercolor="#666666"
- borderopacity="1.0"
- inkscape:pageopacity="0"
- inkscape:pageshadow="2"
- inkscape:zoom="17.223347"
- inkscape:cx="11.892485"
- inkscape:cy="9.029807"
- inkscape:document-units="px"
- inkscape:current-layer="layer25"
- showgrid="false"
- units="px"
- borderlayer="true"
- inkscape:showpageshadow="false"
- inkscape:snap-page="true"
- inkscape:snap-bbox="true"
- inkscape:bbox-nodes="true"
- inkscape:snap-bbox-edge-midpoints="true"
- inkscape:snap-bbox-midpoints="true"
- showguides="true"
- inkscape:guide-bbox="true"
- inkscape:window-width="1920"
- inkscape:window-height="1027"
- inkscape:window-x="-8"
- inkscape:window-y="-8"
- inkscape:window-maximized="1"
- inkscape:snap-center="true"
- inkscape:snap-object-midpoints="true"
- inkscape:measure-start="18.8975,12.498"
- inkscape:measure-end="20,12.498"
- inkscape:snap-intersection-paths="true"
- inkscape:object-paths="true"
- inkscape:snap-smooth-nodes="true"
- inkscape:snap-midpoints="true"
- inkscape:snap-global="true">
- <sodipodi:guide
- position="10,10"
- orientation="1,0"
- id="guide4039"
- inkscape:locked="false" />
- <sodipodi:guide
- position="10,10"
- orientation="0,1"
- id="guide3935"
- inkscape:locked="false" />
- </sodipodi:namedview>
- <metadata
- id="metadata5">
- <rdf:RDF>
- <cc:Work
- rdf:about="">
- <dc:format>image/svg+xml</dc:format>
- <dc:type
- rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
- <dc:title></dc:title>
- </cc:Work>
- </rdf:RDF>
- </metadata>
- <g
- inkscape:groupmode="layer"
- id="layer25"
- inkscape:label="Content">
- <path
- inkscape:connector-curvature="0"
- style="display:inline;opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:1.93939996;stroke-opacity:1;paint-order:markers stroke fill"
- d="M 10,9.9999999e-8 C 4.477152,9.9999999e-8 0,4.4771526 0,10 c 0,5.522848 4.477152,10 10,10 5.522848,0 10,-4.477152 10,-10 0,-5.5228474 -4.477152,-9.9999999 -10,-9.9999999 z M 10,2.5000001 c 4.142136,0 7.5,3.3578645 7.5,7.4999999 0,4.142136 -3.357864,7.5 -7.5,7.5 C 5.857864,17.5 2.5,14.142136 2.5,10 2.5,5.8578646 5.857864,2.5000001 10,2.5000001 Z"
- id="path3872" />
- </g>
-</svg>
diff --git a/src/stylesheets/Paper/Light/unchecked-disabled.svg b/src/stylesheets/Paper/Light/unchecked-disabled.svg
deleted file mode 100644
index 1c9fd840..00000000
--- a/src/stylesheets/Paper/Light/unchecked-disabled.svg
+++ /dev/null
@@ -1,94 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
- xmlns:dc="http://purl.org/dc/elements/1.1/"
- xmlns:cc="http://creativecommons.org/ns#"
- xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
- xmlns:svg="http://www.w3.org/2000/svg"
- xmlns="http://www.w3.org/2000/svg"
- xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
- xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
- width="20"
- height="20"
- viewBox="0 0 20 20"
- version="1.1"
- id="svg8"
- inkscape:export-filename="D:\Bethesda Game Modding\Mod Organizer 2\stylesheets\Paper\Dark\dots.png"
- inkscape:export-xdpi="307.20001"
- inkscape:export-ydpi="307.20001"
- inkscape:version="0.92.1 r15371"
- sodipodi:docname="unchecked-disabled.svg">
- <defs
- id="defs2" />
- <sodipodi:namedview
- id="base"
- pagecolor="#505050"
- bordercolor="#666666"
- borderopacity="1.0"
- inkscape:pageopacity="0"
- inkscape:pageshadow="2"
- inkscape:zoom="13.752717"
- inkscape:cx="12.990894"
- inkscape:cy="5.3273691"
- inkscape:document-units="px"
- inkscape:current-layer="layer25"
- showgrid="false"
- units="px"
- borderlayer="true"
- inkscape:showpageshadow="false"
- inkscape:snap-page="true"
- inkscape:snap-bbox="true"
- inkscape:bbox-nodes="true"
- inkscape:snap-bbox-edge-midpoints="true"
- inkscape:snap-bbox-midpoints="true"
- showguides="true"
- inkscape:guide-bbox="true"
- inkscape:window-width="1920"
- inkscape:window-height="1027"
- inkscape:window-x="-8"
- inkscape:window-y="-8"
- inkscape:window-maximized="1"
- inkscape:snap-center="true"
- inkscape:snap-object-midpoints="true"
- inkscape:measure-start="18.8975,12.498"
- inkscape:measure-end="20,12.498"
- inkscape:snap-intersection-paths="true"
- inkscape:object-paths="true"
- inkscape:snap-smooth-nodes="true"
- inkscape:snap-midpoints="true"
- inkscape:snap-global="true">
- <sodipodi:guide
- position="10,10"
- orientation="1,0"
- id="guide4039"
- inkscape:locked="false" />
- <sodipodi:guide
- position="10,10"
- orientation="0,1"
- id="guide3935"
- inkscape:locked="false" />
- </sodipodi:namedview>
- <metadata
- id="metadata5">
- <rdf:RDF>
- <cc:Work
- rdf:about="">
- <dc:format>image/svg+xml</dc:format>
- <dc:type
- rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
- <dc:title />
- </cc:Work>
- </rdf:RDF>
- </metadata>
- <g
- inkscape:groupmode="layer"
- id="layer25"
- inkscape:label="Content">
- <path
- style="display:inline;opacity:1;fill:#808080;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:169.70561218;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:1.93939996;stroke-opacity:1;paint-order:markers stroke fill"
- d="M 4,-5.0000001e-8 0,3.9999999 l 2,2 L 6,9.9999998 2,14 l -2,2 4,4 2,-2 4,-4 4,4 2,2 4,-4 -2,-2 -4,-4.0000002 4,-3.9999999 2,-2 L 16,-5.0000001e-8 14,2 10,5.9999999 6,2 Z"
- id="path3944"
- inkscape:connector-curvature="0" />
- </g>
-</svg>
diff --git a/src/stylesheets/Paper/Light/unchecked-hover.svg b/src/stylesheets/Paper/Light/unchecked-hover.svg
deleted file mode 100644
index 7e3683dc..00000000
--- a/src/stylesheets/Paper/Light/unchecked-hover.svg
+++ /dev/null
@@ -1,94 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
- xmlns:dc="http://purl.org/dc/elements/1.1/"
- xmlns:cc="http://creativecommons.org/ns#"
- xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
- xmlns:svg="http://www.w3.org/2000/svg"
- xmlns="http://www.w3.org/2000/svg"
- xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
- xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
- width="20"
- height="20"
- viewBox="0 0 20 20"
- version="1.1"
- id="svg8"
- inkscape:export-filename="D:\Bethesda Game Modding\Mod Organizer 2\stylesheets\Paper\Dark\dots.png"
- inkscape:export-xdpi="307.20001"
- inkscape:export-ydpi="307.20001"
- inkscape:version="0.92.1 r15371"
- sodipodi:docname="unchecked-hover.svg">
- <defs
- id="defs2" />
- <sodipodi:namedview
- id="base"
- pagecolor="#505050"
- bordercolor="#666666"
- borderopacity="1.0"
- inkscape:pageopacity="0"
- inkscape:pageshadow="2"
- inkscape:zoom="21.962888"
- inkscape:cx="29.324366"
- inkscape:cy="13.4838"
- inkscape:document-units="px"
- inkscape:current-layer="layer25"
- showgrid="false"
- units="px"
- borderlayer="true"
- inkscape:showpageshadow="false"
- inkscape:snap-page="true"
- inkscape:snap-bbox="true"
- inkscape:bbox-nodes="true"
- inkscape:snap-bbox-edge-midpoints="true"
- inkscape:snap-bbox-midpoints="true"
- showguides="true"
- inkscape:guide-bbox="true"
- inkscape:window-width="1920"
- inkscape:window-height="1027"
- inkscape:window-x="-8"
- inkscape:window-y="-8"
- inkscape:window-maximized="1"
- inkscape:snap-center="true"
- inkscape:snap-object-midpoints="true"
- inkscape:measure-start="18.8975,12.498"
- inkscape:measure-end="20,12.498"
- inkscape:snap-intersection-paths="true"
- inkscape:object-paths="true"
- inkscape:snap-smooth-nodes="true"
- inkscape:snap-midpoints="true"
- inkscape:snap-global="true">
- <sodipodi:guide
- position="10,10"
- orientation="1,0"
- id="guide4039"
- inkscape:locked="false" />
- <sodipodi:guide
- position="10,10"
- orientation="0,1"
- id="guide3935"
- inkscape:locked="false" />
- </sodipodi:namedview>
- <metadata
- id="metadata5">
- <rdf:RDF>
- <cc:Work
- rdf:about="">
- <dc:format>image/svg+xml</dc:format>
- <dc:type
- rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
- <dc:title />
- </cc:Work>
- </rdf:RDF>
- </metadata>
- <g
- inkscape:groupmode="layer"
- id="layer25"
- inkscape:label="Content">
- <path
- inkscape:connector-curvature="0"
- style="display:inline;opacity:1;fill:#008484;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:1.93939996;stroke-opacity:1;paint-order:markers stroke fill"
- d="M 10,9.9999999e-8 C 4.477152,9.9999999e-8 0,4.4771526 0,10 c 0,5.522848 4.477152,10 10,10 5.522848,0 10,-4.477152 10,-10 0,-5.5228474 -4.477152,-9.9999999 -10,-9.9999999 z M 10,2.5000001 c 4.142136,0 7.5,3.3578645 7.5,7.4999999 0,4.142136 -3.357864,7.5 -7.5,7.5 C 5.857864,17.5 2.5,14.142136 2.5,10 2.5,5.8578646 5.857864,2.5000001 10,2.5000001 Z"
- id="path3872" />
- </g>
-</svg>
diff --git a/src/stylesheets/Paper/Light/unchecked.svg b/src/stylesheets/Paper/Light/unchecked.svg
deleted file mode 100644
index 9e1f8b08..00000000
--- a/src/stylesheets/Paper/Light/unchecked.svg
+++ /dev/null
@@ -1,94 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
- xmlns:dc="http://purl.org/dc/elements/1.1/"
- xmlns:cc="http://creativecommons.org/ns#"
- xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
- xmlns:svg="http://www.w3.org/2000/svg"
- xmlns="http://www.w3.org/2000/svg"
- xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
- xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
- width="20"
- height="20"
- viewBox="0 0 20 20"
- version="1.1"
- id="svg8"
- inkscape:export-filename="D:\Bethesda Game Modding\Mod Organizer 2\stylesheets\Paper\Dark\dots.png"
- inkscape:export-xdpi="307.20001"
- inkscape:export-ydpi="307.20001"
- inkscape:version="0.92.1 r15371"
- sodipodi:docname="unchecked.svg">
- <defs
- id="defs2" />
- <sodipodi:namedview
- id="base"
- pagecolor="#505050"
- bordercolor="#666666"
- borderopacity="1.0"
- inkscape:pageopacity="0"
- inkscape:pageshadow="2"
- inkscape:zoom="20.964275"
- inkscape:cx="11.984896"
- inkscape:cy="5.7816586"
- inkscape:document-units="px"
- inkscape:current-layer="layer25"
- showgrid="false"
- units="px"
- borderlayer="true"
- inkscape:showpageshadow="false"
- inkscape:snap-page="true"
- inkscape:snap-bbox="true"
- inkscape:bbox-nodes="true"
- inkscape:snap-bbox-edge-midpoints="true"
- inkscape:snap-bbox-midpoints="true"
- showguides="true"
- inkscape:guide-bbox="true"
- inkscape:window-width="1920"
- inkscape:window-height="1027"
- inkscape:window-x="-8"
- inkscape:window-y="-8"
- inkscape:window-maximized="1"
- inkscape:snap-center="true"
- inkscape:snap-object-midpoints="true"
- inkscape:measure-start="18.8975,12.498"
- inkscape:measure-end="20,12.498"
- inkscape:snap-intersection-paths="true"
- inkscape:object-paths="true"
- inkscape:snap-smooth-nodes="true"
- inkscape:snap-midpoints="true"
- inkscape:snap-global="true">
- <sodipodi:guide
- position="10,10"
- orientation="1,0"
- id="guide4039"
- inkscape:locked="false" />
- <sodipodi:guide
- position="10,10"
- orientation="0,1"
- id="guide3935"
- inkscape:locked="false" />
- </sodipodi:namedview>
- <metadata
- id="metadata5">
- <rdf:RDF>
- <cc:Work
- rdf:about="">
- <dc:format>image/svg+xml</dc:format>
- <dc:type
- rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
- <dc:title />
- </cc:Work>
- </rdf:RDF>
- </metadata>
- <g
- inkscape:groupmode="layer"
- id="layer25"
- inkscape:label="Content">
- <path
- inkscape:connector-curvature="0"
- style="display:inline;opacity:1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:1.93939996;stroke-opacity:1;paint-order:markers stroke fill"
- d="M 10,9.9999999e-8 C 4.477152,9.9999999e-8 0,4.4771526 0,10 c 0,5.522848 4.477152,10 10,10 5.522848,0 10,-4.477152 10,-10 0,-5.5228474 -4.477152,-9.9999999 -10,-9.9999999 z M 10,2.5000001 c 4.142136,0 7.5,3.3578645 7.5,7.4999999 0,4.142136 -3.357864,7.5 -7.5,7.5 C 5.857864,17.5 2.5,14.142136 2.5,10 2.5,5.8578646 5.857864,2.5000001 10,2.5000001 Z"
- id="path3872" />
- </g>
-</svg>
diff --git a/src/stylesheets/Transparent-Style-101-Green.qss b/src/stylesheets/Transparent-Style-101-Green.qss
index a4ed2623..3bbdb5db 100644
--- a/src/stylesheets/Transparent-Style-101-Green.qss
+++ b/src/stylesheets/Transparent-Style-101-Green.qss
@@ -485,6 +485,11 @@ QStatusBar::item {border: None;}
font-family: Source Sans Pro;
font-size: 14px;
}
+
+LinkLabel {
+ qproperty-linkColor: #3399FF;
+}
+
QAbstractItemView {
color: #cccccc;
font-family: Source Sans Pro;
diff --git a/src/stylesheets/Transparent-Style-BOS.qss b/src/stylesheets/Transparent-Style-BOS.qss
index efad0859..33eed327 100644
--- a/src/stylesheets/Transparent-Style-BOS.qss
+++ b/src/stylesheets/Transparent-Style-BOS.qss
@@ -743,3 +743,7 @@ QStatusBar::item {border: None;}
border-bottom:1px solid #9A9A00;
*/
}
+
+LinkLabel {
+ qproperty-linkColor: #3399FF;
+}
diff --git a/src/stylesheets/Transparent-Style-Skyrim.qss b/src/stylesheets/Transparent-Style-Skyrim.qss
index 89e36c74..308580a5 100644
--- a/src/stylesheets/Transparent-Style-Skyrim.qss
+++ b/src/stylesheets/Transparent-Style-Skyrim.qss
@@ -743,3 +743,7 @@ QStatusBar::item {border: None;}
border-bottom:1px solid #9A9A00;
*/
}
+
+LinkLabel {
+ qproperty-linkColor: #3399FF;
+}
diff --git a/src/stylesheets/dark.qss b/src/stylesheets/dark.qss
index 22cd598c..91f808bc 100644
--- a/src/stylesheets/dark.qss
+++ b/src/stylesheets/dark.qss
@@ -349,7 +349,7 @@ QToolButton:hover
padding-bottom: 2px;
}
-QTreeView
+QTreeView, QListView
{
color: #E9E6E4;
background-color: #3F4041;
@@ -380,4 +380,8 @@ DownloadListWidget[downloadView=standard]::item {
DownloadListWidget[downloadView=compact]::item {
padding: 4px;
-} \ No newline at end of file
+}
+
+LinkLabel {
+ qproperty-linkColor: #3399FF;
+}
diff --git a/src/stylesheets/dracula.qss b/src/stylesheets/dracula.qss
index 2a7fbf9e..a78d2414 100644
--- a/src/stylesheets/dracula.qss
+++ b/src/stylesheets/dracula.qss
@@ -4,7 +4,7 @@
QWidget, QStackedWidget,
QScrollArea, QAbstractScrollArea {
- background-color: transparent;
+ background-color: #3c3f41;
color: #bbbbbb;
}
@@ -30,6 +30,10 @@ QCheckBox, QGroupBox {
selection-color: #bbbbbb;
}
+LinkLabel {
+ qproperty-linkColor: #3399FF;
+}
+
/*
* GroupBox and CheckBox
*/
diff --git a/src/stylesheets/skyrim.qss b/src/stylesheets/skyrim.qss
index 2da5154d..a516f5ff 100644
--- a/src/stylesheets/skyrim.qss
+++ b/src/stylesheets/skyrim.qss
@@ -113,6 +113,9 @@ QListView::item {
QLabel {
background-color: transparent; }
+LinkLabel {
+ qproperty-linkColor: #3399FF; }
+
/* Font size */
QLabel,
QTextEdit,
diff --git a/src/stylesheets/vs15 Dark-Green.qss b/src/stylesheets/vs15 Dark-Green.qss
index 9cf26a31..6d95c6cc 100644
--- a/src/stylesheets/vs15 Dark-Green.qss
+++ b/src/stylesheets/vs15 Dark-Green.qss
@@ -94,6 +94,9 @@ QTreeView::branch:selected {
QLabel {
background-color: transparent; }
+LinkLabel {
+ qproperty-linkColor: #3399FF; }
+
/* Left Pane & File Trees #QTreeView, #QListView*/
QTreeView::branch:closed:has-children {
image: url(./vs15/branch-closed.png); }
@@ -209,6 +212,13 @@ QToolButton {
QToolButton:pressed {
background-color: #009933; }
+QToolButton::menu-indicator {
+ image: url(./vs15/combobox-down.png);
+ subcontrol-origin: padding;
+ subcontrol-position: center right;
+ padding-top: 10%;
+ padding-right: 5%; }
+
/* Group Boxes #QGroupBox */
QGroupBox {
border-color: #3F3F46;
diff --git a/src/stylesheets/vs15 Dark-Orange.qss b/src/stylesheets/vs15 Dark-Orange.qss
index 041f1b00..2dd27df4 100644
--- a/src/stylesheets/vs15 Dark-Orange.qss
+++ b/src/stylesheets/vs15 Dark-Orange.qss
@@ -95,6 +95,9 @@ QTreeView::branch:selected {
QLabel {
background-color: transparent; }
+LinkLabel {
+ qproperty-linkColor: #3399FF; }
+
/* Left Pane & File Trees #QTreeView, #QListView*/
QTreeView::branch:closed:has-children {
image: url(./vs15/branch-closed.png); }
@@ -210,6 +213,13 @@ QToolButton {
QToolButton:pressed {
background-color: #CC6600; }
+QToolButton::menu-indicator {
+ image: url(./vs15/combobox-down.png);
+ subcontrol-origin: padding;
+ subcontrol-position: center right;
+ padding-top: 10%;
+ padding-right: 5%; }
+
/* Group Boxes #QGroupBox */
QGroupBox {
border-color: #3F3F46;
diff --git a/src/stylesheets/vs15 Dark-Purple.qss b/src/stylesheets/vs15 Dark-Purple.qss
index bc8bbcde..116aaa7d 100644
--- a/src/stylesheets/vs15 Dark-Purple.qss
+++ b/src/stylesheets/vs15 Dark-Purple.qss
@@ -95,6 +95,9 @@ QTreeView::branch:selected {
QLabel {
background-color: transparent; }
+LinkLabel {
+ qproperty-linkColor: #3399FF; }
+
/* Left Pane & File Trees #QTreeView, #QListView*/
QTreeView::branch:closed:has-children {
image: url(./vs15/branch-closed.png); }
@@ -210,6 +213,13 @@ QToolButton {
QToolButton:pressed {
background-color: #7E2AD2; }
+QToolButton::menu-indicator {
+ image: url(./vs15/combobox-down.png);
+ subcontrol-origin: padding;
+ subcontrol-position: center right;
+ padding-top: 10%;
+ padding-right: 5%; }
+
/* Group Boxes #QGroupBox */
QGroupBox {
border-color: #3F3F46;
diff --git a/src/stylesheets/vs15 Dark-Red.qss b/src/stylesheets/vs15 Dark-Red.qss
index 0c9143cd..60f565a1 100644
--- a/src/stylesheets/vs15 Dark-Red.qss
+++ b/src/stylesheets/vs15 Dark-Red.qss
@@ -95,6 +95,9 @@ QTreeView::branch:selected {
QLabel {
background-color: transparent; }
+LinkLabel {
+ qproperty-linkColor: #3399FF; }
+
/* Left Pane & File Trees #QTreeView, #QListView*/
QTreeView::branch:closed:has-children {
image: url(./vs15/branch-closed.png); }
@@ -210,6 +213,13 @@ QToolButton {
QToolButton:pressed {
background-color: #990000; }
+QToolButton::menu-indicator {
+ image: url(./vs15/combobox-down.png);
+ subcontrol-origin: padding;
+ subcontrol-position: center right;
+ padding-top: 10%;
+ padding-right: 5%; }
+
/* Group Boxes #QGroupBox */
QGroupBox {
border-color: #3F3F46;
diff --git a/src/stylesheets/vs15 Dark-Yellow.qss b/src/stylesheets/vs15 Dark-Yellow.qss
index 2eb42534..bfaa4c94 100644
--- a/src/stylesheets/vs15 Dark-Yellow.qss
+++ b/src/stylesheets/vs15 Dark-Yellow.qss
@@ -95,6 +95,9 @@ QTreeView::branch:selected {
QLabel {
background-color: transparent; }
+LinkLabel {
+ qproperty-linkColor: #3399FF; }
+
/* Left Pane & File Trees #QTreeView, #QListView*/
QTreeView::branch:closed:has-children {
image: url(./vs15/branch-closed.png); }
@@ -210,6 +213,13 @@ QToolButton {
QToolButton:pressed {
background-color: #9A9A00; }
+QToolButton::menu-indicator {
+ image: url(./vs15/combobox-down.png);
+ subcontrol-origin: padding;
+ subcontrol-position: center right;
+ padding-top: 10%;
+ padding-right: 5%; }
+
/* Group Boxes #QGroupBox */
QGroupBox {
border-color: #3F3F46;
diff --git a/src/stylesheets/vs15 Dark.qss b/src/stylesheets/vs15 Dark.qss
index d67cce35..1d27be17 100644
--- a/src/stylesheets/vs15 Dark.qss
+++ b/src/stylesheets/vs15 Dark.qss
@@ -94,6 +94,9 @@ QTreeView::branch:selected {
QLabel {
background-color: transparent; }
+LinkLabel {
+ qproperty-linkColor: #3399FF; }
+
/* Left Pane & File Trees #QTreeView, #QListView*/
QTreeView::branch:closed:has-children {
image: url(./vs15/branch-closed.png); }
@@ -209,6 +212,13 @@ QToolButton {
QToolButton:pressed {
background-color: #3399FF; }
+QToolButton::menu-indicator {
+ image: url(./vs15/combobox-down.png);
+ subcontrol-origin: padding;
+ subcontrol-position: center right;
+ padding-top: 10%;
+ padding-right: 5%; }
+
/* Group Boxes #QGroupBox */
QGroupBox {
border-color: #3F3F46;
diff --git a/src/syncoverwritedialog.cpp b/src/syncoverwritedialog.cpp
index 4ee4716e..b1643b2d 100644
--- a/src/syncoverwritedialog.cpp
+++ b/src/syncoverwritedialog.cpp
@@ -22,6 +22,7 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#include "ui_syncoverwritedialog.h"
#include <utility.h>
#include <report.h>
+#include <log.h>
#include <QDir>
#include <QDirIterator>
@@ -86,7 +87,7 @@ void SyncOverwriteDialog::readTree(const QString &path, DirectoryEntry *director
if (subDir != nullptr) {
readTree(fileInfo.absoluteFilePath(), subDir, newItem);
} else {
- qCritical("no directory structure for %s?", qUtf8Printable(file));
+ log::error("no directory structure for {}?", file);
delete newItem;
newItem = nullptr;
}
diff --git a/src/texteditor.cpp b/src/texteditor.cpp
index 130cd76f..4a8080f4 100644
--- a/src/texteditor.cpp
+++ b/src/texteditor.cpp
@@ -1,7 +1,10 @@
#include "texteditor.h"
#include "utility.h"
+#include <log.h>
#include <QSplitter>
+using namespace MOBase;
+
TextEditor::TextEditor(QWidget* parent) :
QPlainTextEdit(parent),
m_toolbar(nullptr), m_lineNumbers(nullptr), m_highlighter(nullptr),
@@ -196,7 +199,7 @@ void TextEditor::explore()
return;
}
- MOBase::shell::ExploreFile(m_filename);
+ shell::Explore(m_filename);
}
void TextEditor::onModified(bool b)
@@ -249,7 +252,7 @@ QWidget* TextEditor::wrapEditWidget()
auto index = splitter->indexOf(this);
if (index == -1) {
- qCritical(
+ log::error(
"TextEditor: cannot wrap edit widget to display a toolbar, "
"parent is a splitter, but widget isn't in it");
@@ -260,7 +263,7 @@ QWidget* TextEditor::wrapEditWidget()
} else {
// unknown parent
- qCritical(
+ log::error(
"TextEditor: cannot wrap edit widget to display a toolbar, "
"no parent or parent has no layout");
diff --git a/src/transfersavesdialog.cpp b/src/transfersavesdialog.cpp
index 130df14f..6b8f7bd0 100644
--- a/src/transfersavesdialog.cpp
+++ b/src/transfersavesdialog.cpp
@@ -24,6 +24,7 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#include "isavegame.h"
#include "savegameinfo.h"
#include <utility.h>
+#include <log.h>
#include <QtDebug>
#include <QDateTime>
@@ -61,7 +62,7 @@ public:
virtual QDateTime getCreationTime() const override
{
- return QFileInfo(m_File).created();
+ return QFileInfo(m_File).birthTime();
}
virtual QString getSaveGroupIdentifier() const override
@@ -186,7 +187,7 @@ void TransferSavesDialog::on_moveToLocalBtn_clicked()
[this](const QString &source, const QString &destination) -> bool {
return shellMove(source, destination, this);
},
- "Failed to move %s to %s")) {
+ "Failed to move {} to {}")) {
refreshGlobalSaves();
refreshGlobalCharacters();
refreshLocalSaves();
@@ -203,7 +204,7 @@ void TransferSavesDialog::on_copyToLocalBtn_clicked()
[this](const QString &source, const QString &destination) -> bool {
return shellCopy(source, destination, this);
},
- "Failed to copy %s to %s")) {
+ "Failed to copy {} to {}")) {
refreshLocalSaves();
refreshLocalCharacters();
}
@@ -218,7 +219,7 @@ void TransferSavesDialog::on_moveToGlobalBtn_clicked()
[this](const QString &source, const QString &destination) -> bool {
return shellMove(source, destination, this);
},
- "Failed to move %s to %s")) {
+ "Failed to move {} to {}")) {
refreshGlobalSaves();
refreshGlobalCharacters();
refreshLocalSaves();
@@ -235,7 +236,7 @@ void TransferSavesDialog::on_copyToGlobalBtn_clicked()
[this](const QString &source, const QString &destination) -> bool {
return shellCopy(source, destination, this);
},
- "Failed to copy %s to %s")) {
+ "Failed to copy {} to {}")) {
refreshGlobalSaves();
refreshGlobalCharacters();
}
@@ -340,9 +341,7 @@ bool TransferSavesDialog::transferCharacters(
}
if (!method(sourceFile.absoluteFilePath(), destinationFile)) {
- qCritical(errmsg,
- sourceFile.absoluteFilePath().toUtf8().constData(),
- qUtf8Printable(destinationFile));
+ log::error(errmsg, sourceFile.absoluteFilePath(), destinationFile);
}
}
}
diff --git a/src/tutorials/tutorial_primer_main.js b/src/tutorials/tutorial_primer_main.js
index 7972cca4..95faa33d 100644
--- a/src/tutorials/tutorial_primer_main.js
+++ b/src/tutorials/tutorial_primer_main.js
@@ -80,7 +80,7 @@ function setupTooptips() {
tooltipWidget("displayCategoriesBtn", qsTr("Show/hide the category pane."))
tooltipWidget("modFilterEdit", qsTr("Quickly filter the mod list as you type."))
tooltipWidget("qt_tabwidget_tabbar", qsTr("Switch between information views."), 0, true)
- tooltipWidget("categoriesList", qsTr("This shows mod categories and some meta categories (in angle-brackets). Select some to filter the mod list. For example select \"<Checked>\" to show only active mods."))
+ tooltipWidget("categoriesGroup", qsTr("This shows mod categories and some meta categories (in angle-brackets). Select some to filter the mod list. For example select \"<Checked>\" to show only active mods."))
tooltipWidget("executablesListBox", qsTr("Customizable list for choosing the program to run."))
tooltipWidget("startButton", qsTr("When this button is clicked, Mod Organizer creates a virtual directory structure then runs the program selected to the left."))
tooltipWidget("linkButton", qsTr("Will create a shortcut for quick access. The shortcut can be placed in the toolbar at the top, in the Start Menu or on the Windows Desktop."))
diff --git a/src/uilocker.cpp b/src/uilocker.cpp
new file mode 100644
index 00000000..07fe7f1b
--- /dev/null
+++ b/src/uilocker.cpp
@@ -0,0 +1,617 @@
+#include "uilocker.h"
+#include "mainwindow.h"
+#include <QGraphicsDropShadowEffect>
+#include <QMenuBar>
+#include <QStatusBar>
+
+class UILockerInterface
+{
+public:
+ UILockerInterface(QWidget* mainUI) :
+ m_mainUI(mainUI), m_target(nullptr), m_message(nullptr), m_info(nullptr),
+ m_buttons(nullptr), m_reason(UILocker::NoReason)
+ {
+ m_timer.reset(new QTimer);
+ QObject::connect(m_timer.get(), &QTimer::timeout, [&]{ checkTarget(); });
+ m_timer->start(200);
+
+ set();
+ }
+
+ ~UILockerInterface()
+ {
+ if (m_topLevel) {
+ delete m_topLevel.data();
+ }
+ }
+
+ void checkTarget()
+ {
+ if (set()) {
+ update(m_reason);
+ }
+ }
+
+ bool set()
+ {
+ QWidget* newTarget = findTarget();
+ if (m_topLevel && newTarget == m_target) {
+ return false;
+ }
+
+ m_target = newTarget;
+
+ QFrame* center = nullptr;
+
+ if (m_target) {
+ center = createOverlay(m_target);
+ } else {
+ center = createDialog();
+ }
+
+ createMessageLabel();
+ createInfoLabel();
+ createButtonsPanel();
+
+ center->layout()->addWidget(m_message);
+ center->layout()->addWidget(m_info);
+ center->layout()->addWidget(m_buttons);
+
+ m_topLevel->setFocusPolicy(Qt::TabFocus);
+ m_topLevel->setFocus();
+ m_topLevel->show();
+ m_topLevel->setEnabled(true);
+
+ m_topLevel->raise();
+ m_topLevel->activateWindow();
+
+ return true;
+ }
+
+ void update(UILocker::Reasons reason)
+ {
+ m_reason = reason;
+ updateMessage(reason);
+ updateButtons(reason);
+ setInfo(m_labels);
+ }
+
+ void setInfo(const QStringList& labels)
+ {
+ const int MaxLabels = 2;
+
+ m_labels = labels;
+
+ QString s;
+
+ if (labels.size() > MaxLabels) {
+ s = labels.mid(0, MaxLabels).join(", ") + "...";
+ } else {
+ s = labels.join(", ");
+ }
+
+ m_info->setText(s);
+ }
+
+ QWidget* topLevel()
+ {
+ return m_topLevel.data();
+ }
+
+private:
+ class Filter : public QObject
+ {
+ public:
+ std::function<void ()> resized;
+ std::function<void ()> closed;
+
+ protected:
+ bool eventFilter(QObject* o, QEvent* e) override
+ {
+ if (e->type() == QEvent::Resize) {
+ if (resized) {
+ resized();
+ }
+ } else if (e->type() == QEvent::Close) {
+ if (closed) {
+ closed();
+ }
+ }
+
+ return QObject::eventFilter(o, e);
+ }
+ };
+
+
+ std::unique_ptr<QTimer> m_timer;
+ QWidget* m_mainUI;
+ QWidget* m_target;
+ QPointer<QWidget> m_topLevel;
+ QLabel* m_message;
+ QLabel* m_info;
+ QStringList m_labels;
+ QWidget* m_buttons;
+ std::unique_ptr<Filter> m_filter;
+ UILocker::Reasons m_reason;
+
+
+ bool hasMainUI() const
+ {
+ return (m_target != nullptr);
+ }
+
+ QWidget* findTarget()
+ {
+ auto isValidTarget = [](QWidget* w) {
+ // skip message boxes
+ if (dynamic_cast<QMessageBox*>(w)) {
+ return false;
+ }
+
+ // skip invisible widgets
+ if (!w->isVisible()) {
+ return false;
+ }
+
+ // skip windows that are too small
+ if (w->height() < 150) {
+ return false;
+ }
+
+ return true;
+ };
+
+
+ // find a modal dialog
+ QWidget* w = QApplication::activeModalWidget();
+
+ while (w && w != m_mainUI) {
+ if (isValidTarget(w)) {
+ return w;
+ }
+
+ w = w->parentWidget();
+ }
+
+ // find a non-modal dialog that's a child of the main window
+ if (m_mainUI) {
+ const auto topLevels = QApplication::topLevelWidgets();
+
+ for (auto* w : topLevels) {
+ if (w && w->parentWidget() == m_mainUI) {
+ if (isValidTarget(w)) {
+ return w;
+ }
+ }
+ }
+ }
+
+ return m_mainUI;
+ }
+
+ QWidget* createTransparentWidget(QWidget* parent=nullptr)
+ {
+ auto* w = new QWidget(parent);
+
+ w->setWindowOpacity(0);
+ w->setAttribute(Qt::WA_NoSystemBackground);
+ w->setAttribute(Qt::WA_TranslucentBackground);
+
+ return w;
+ }
+
+ QFrame* createOverlay(QWidget* mainUI)
+ {
+ if (m_topLevel) {
+ delete m_topLevel.data();
+ m_topLevel.clear();
+ }
+
+ m_topLevel = createTransparentWidget(mainUI);
+ m_topLevel->setWindowFlags(m_topLevel->windowFlags() & Qt::FramelessWindowHint);
+ m_topLevel->setGeometry(mainUI->rect());
+
+ m_filter.reset(new Filter);
+ m_filter->resized = [=]{ m_topLevel->setGeometry(mainUI->rect()); };
+ m_filter->closed = [=]{ checkTarget(); };
+
+ mainUI->installEventFilter(m_filter.get());
+
+ return createFrame();
+ }
+
+ QFrame* createDialog()
+ {
+ if (m_topLevel) {
+ delete m_topLevel.data();
+ m_topLevel.clear();
+ }
+
+ m_topLevel = new QDialog;
+
+ return createFrame();
+ }
+
+ QFrame* createFrame()
+ {
+ auto* frame = new QFrame;
+ auto* ly = new QVBoxLayout(frame);
+
+ if (hasMainUI()) {
+ frame->setFrameStyle(QFrame::StyledPanel);
+ frame->setLineWidth(1);
+ frame->setAutoFillBackground(true);
+
+ auto* shadow = new QGraphicsDropShadowEffect;
+ shadow->setBlurRadius(50);
+ shadow->setOffset(0);
+ shadow->setColor(QColor(0, 0, 0, 100));
+ frame->setGraphicsEffect(shadow);
+ } else {
+ ly->setContentsMargins(0, 0, 0, 0);
+ }
+
+ auto* grid = new QGridLayout(m_topLevel.data());
+ grid->addWidget(createTransparentWidget(), 0, 1);
+ grid->addWidget(createTransparentWidget(), 2, 1);
+ grid->addWidget(createTransparentWidget(), 1, 0);
+ grid->addWidget(createTransparentWidget(), 1, 2);
+ grid->addWidget(frame, 1, 1);
+
+ if (!hasMainUI()) {
+ grid->setContentsMargins(0, 0, 0, 0);
+ }
+
+ grid->setRowStretch(0, 1);
+ grid->setRowStretch(2, 1);
+ grid->setColumnStretch(0, 1);
+ grid->setColumnStretch(2, 1);
+
+ return frame;
+ }
+
+ void createMessageLabel()
+ {
+ m_message = new QLabel;
+ m_message->setAlignment(Qt::AlignCenter | Qt::AlignHCenter);
+ }
+
+ void createInfoLabel()
+ {
+ m_info = new QLabel(" ");
+ m_info->setAlignment(Qt::AlignCenter | Qt::AlignHCenter);
+ }
+
+ void createButtonsPanel()
+ {
+ m_buttons = new QWidget;
+ m_buttons->setLayout(new QHBoxLayout);
+ }
+
+
+ void updateMessage(UILocker::Reasons reason)
+ {
+ switch (reason)
+ {
+ case UILocker::LockUI:
+ {
+ QString s;
+
+ if (hasMainUI()) {
+ s = QObject::tr(
+ "Mod Organizer is locked while the application is running.");
+ } else {
+ s = QObject::tr("Mod Organizer is currently running an application.");
+ }
+
+ m_message->setText(s);
+
+ break;
+ }
+
+ case UILocker::OutputRequired:
+ {
+ m_message->setText(QObject::tr(
+ "The application must run to completion because its output is "
+ "required."));
+
+ break;
+ }
+
+ case UILocker::PreventExit:
+ {
+ m_message->setText(QObject::tr(
+ "Mod Organizer is waiting on application to close before exiting."));
+
+ break;
+ }
+ }
+ }
+
+ void updateButtons(UILocker::Reasons reason)
+ {
+ MOBase::deleteChildWidgets(m_buttons);
+ auto* ly = m_buttons->layout();
+
+ switch (reason)
+ {
+ case UILocker::LockUI: // fall-through
+ case UILocker::OutputRequired:
+ {
+ auto* unlock = new QPushButton(QObject::tr("Unlock"));
+
+ QObject::connect(unlock, &QPushButton::clicked, [&]{
+ UILocker::instance().onForceUnlock();
+ });
+
+ ly->addWidget(unlock);
+
+ break;
+ }
+
+ case UILocker::PreventExit:
+ {
+ auto* exit = new QPushButton(QObject::tr("Exit Now"));
+ QObject::connect(exit, &QPushButton::clicked, [&]{
+ UILocker::instance().onForceUnlock();
+ });
+
+ ly->addWidget(exit);
+
+ auto* cancel = new QPushButton(QObject::tr("Cancel"));
+ QObject::connect(cancel, &QPushButton::clicked, [&]{
+ UILocker::instance().onCancel();
+ });
+
+ ly->addWidget(cancel);
+
+ break;
+ }
+ }
+ }
+};
+
+UILocker::Session::~Session()
+{
+ unlock();
+}
+
+void UILocker::Session::unlock()
+{
+ QMetaObject::invokeMethod(qApp, [this]{
+ UILocker::instance().unlock(this);
+ });
+}
+
+void UILocker::Session::setInfo(DWORD pid, const QString& name)
+{
+ {
+ std::scoped_lock lock(m_mutex);
+ m_pid = pid;
+ m_name = name;
+ }
+
+ QMetaObject::invokeMethod(qApp, [this]{
+ UILocker::instance().updateLabel();
+ });
+}
+
+DWORD UILocker::Session::pid() const
+{
+ std::scoped_lock lock(m_mutex);
+ return m_pid;
+}
+
+const QString& UILocker::Session::name() const
+{
+ std::scoped_lock lock(m_mutex);
+ return m_name;
+}
+
+UILocker::Results UILocker::Session::result() const
+{
+ return UILocker::instance().result();
+}
+
+
+static UILocker* g_instance = nullptr;
+
+
+UILocker::UILocker()
+ : m_parent(nullptr), m_result(NoResult)
+{
+ Q_ASSERT(!g_instance);
+ g_instance = this;
+}
+
+UILocker::~UILocker()
+{
+ const auto v = m_sessions;
+
+ for (auto& wp : v) {
+ if (auto s=wp.lock()) {
+ unlock(s.get());
+ }
+ }
+
+ g_instance = nullptr;
+}
+
+UILocker& UILocker::instance()
+{
+ Q_ASSERT(g_instance);
+ return *g_instance;
+}
+
+void UILocker::setUserInterface(QWidget* parent)
+{
+ m_parent = parent;
+}
+
+std::shared_ptr<UILocker::Session> UILocker::lock(Reasons reason)
+{
+ m_result = StillLocked;
+ createUi(reason);
+
+ auto ls = std::make_shared<Session>();
+ m_sessions.push_back(ls);
+
+ updateLabel();
+
+ return ls;
+}
+
+bool UILocker::locked() const
+{
+ return !m_sessions.empty();
+}
+
+void UILocker::unlock(Session* s)
+{
+ auto itor = m_sessions.begin();
+ for (;;) {
+ if (itor == m_sessions.end()) {
+ break;
+ }
+
+ if (auto ss=itor->lock()) {
+ if (ss.get() == s) {
+ itor = m_sessions.erase(itor);
+ continue;
+ }
+ } else {
+ itor = m_sessions.erase(itor);
+ continue;
+ }
+
+ ++itor;
+ }
+
+ if (m_sessions.empty()) {
+ m_ui.reset();
+ enableAll();
+ } else {
+ updateLabel();
+ }
+}
+
+void UILocker::unlockCurrent()
+{
+ if (m_sessions.empty()) {
+ return;
+ }
+
+ auto s = m_sessions.back().lock();
+ if (!s) {
+ m_sessions.pop_back();
+ return;
+ }
+
+ unlock(s.get());
+}
+
+void UILocker::updateLabel()
+{
+ if (!m_ui) {
+ // this can happen if the lock overlay was destroyed while a cross-thread
+ // call for updateLabel() was in flight
+ return;
+ }
+
+ QStringList labels;
+
+ for (auto itor=m_sessions.rbegin(); itor!=m_sessions.rend(); ++itor) {
+ if (auto ss=itor->lock()) {
+ labels.push_back(QString("%1 (%2)").arg(ss->name()).arg(ss->pid()));
+ }
+ }
+
+ m_ui->setInfo(labels);
+}
+
+UILocker::Results UILocker::result() const
+{
+ return m_result;
+}
+
+void UILocker::createUi(Reasons reason)
+{
+ if (!m_ui) {
+ m_ui.reset(new UILockerInterface(m_parent));
+ }
+
+ m_ui->update(reason);
+
+ disableAll();
+}
+
+void UILocker::onForceUnlock()
+{
+ m_result = ForceUnlocked;
+ unlockCurrent();
+}
+
+void UILocker::onCancel()
+{
+ m_result = Cancelled;
+ unlockCurrent();
+}
+
+template <class T>
+QList<T> findChildrenImmediate(QWidget* parent)
+{
+ return parent->findChildren<T>(QString(), Qt::FindDirectChildrenOnly);
+}
+
+void UILocker::disableAll()
+{
+ const auto topLevels = QApplication::topLevelWidgets();
+
+ for (auto* w : topLevels) {
+ if (auto* mw=dynamic_cast<QMainWindow*>(w)) {
+ disable(mw->centralWidget());
+ disable(mw->menuBar());
+ disable(mw->statusBar());
+
+ for (auto* tb : findChildrenImmediate<QToolBar*>(w)) {
+ disable(tb);
+ }
+
+ for (auto* d : findChildrenImmediate<QDockWidget*>(w)) {
+ disable(d);
+ }
+ }
+
+ if (auto* d=dynamic_cast<QDialog*>(w)) {
+ // don't disable stuff if this dialog is the overlay, which happens when
+ // there's no ui
+ if (d != m_ui->topLevel()) {
+ // no central widget, just disable the children, except for the overlay
+ for (auto* child : findChildrenImmediate<QWidget*>(d)) {
+ if (child != m_ui->topLevel()) {
+ disable(child);
+ }
+ }
+ }
+ }
+ }
+}
+
+void UILocker::enableAll()
+{
+ for (auto w : m_disabled) {
+ if (w) {
+ w->setEnabled(true);
+ }
+ }
+
+ m_disabled.clear();
+}
+
+void UILocker::disable(QWidget* w)
+{
+ if (w->isEnabled()) {
+ w->setEnabled(false);
+ m_disabled.push_back(w);
+ }
+}
diff --git a/src/uilocker.h b/src/uilocker.h
new file mode 100644
index 00000000..cc467184
--- /dev/null
+++ b/src/uilocker.h
@@ -0,0 +1,96 @@
+#pragma once
+
+#include <QMainWindow>
+#include <mutex>
+
+class UILockerInterface;
+
+class UILocker
+{
+ friend class UILockerInterface;
+
+public:
+ // reason to show the widget
+ //
+ enum Reasons
+ {
+ NoReason = 0,
+
+ // lock the ui
+ LockUI,
+
+ // because the output is required
+ OutputRequired,
+
+ // to prevent exiting until all processes are completed
+ PreventExit
+ };
+
+ // returned by result()
+ //
+ enum Results
+ {
+ NoResult = 0,
+
+ // the widget is still up
+ StillLocked,
+
+ // force unlock was clicked
+ ForceUnlocked,
+
+ // cancel was clicked
+ Cancelled
+ };
+
+
+ class Session
+ {
+ public:
+ ~Session();
+
+ void unlock();
+ void setInfo(DWORD pid, const QString& name);
+ Results result() const;
+
+ DWORD pid() const;
+ const QString& name() const;
+
+ private:
+ mutable std::mutex m_mutex;
+ DWORD m_pid;
+ QString m_name;
+ };
+
+
+ UILocker();
+ ~UILocker();
+
+ static UILocker& instance();
+
+ void setUserInterface(QWidget* parent);
+
+ std::shared_ptr<Session> lock(Reasons reason);
+ bool locked() const;
+
+ Results result() const;
+
+private:
+ QWidget* m_parent;
+ std::unique_ptr<UILockerInterface> m_ui;
+ std::vector<std::weak_ptr<Session>> m_sessions;
+ std::atomic<Results> m_result;
+ std::vector<QPointer<QWidget>> m_disabled;
+
+ void createUi(Reasons reason);
+
+ void unlockCurrent();
+ void unlock(Session* s);
+ void updateLabel();
+
+ void onForceUnlock();
+ void onCancel();
+
+ void disableAll();
+ void enableAll();
+ void disable(QWidget* w);
+};
diff --git a/src/usvfsconnector.cpp b/src/usvfsconnector.cpp
index b752667d..59880754 100644
--- a/src/usvfsconnector.cpp
+++ b/src/usvfsconnector.cpp
@@ -20,6 +20,7 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#include "usvfsconnector.h"
#include "settings.h"
#include "organizercore.h"
+#include "envmodule.h"
#include "shared/util.h"
#include <memory>
#include <sstream>
@@ -32,7 +33,7 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#include <qstandardpaths.h>
static const char SHMID[] = "mod_organizer_instance";
-
+using namespace MOBase;
std::string to_hex(void *bufferIn, size_t bufferSize)
{
@@ -60,8 +61,7 @@ LogWorker::LogWorker()
"yyyy-MM-dd_hh-mm-ss")))
{
m_LogFile.open(QIODevice::WriteOnly);
- qDebug("usvfs log messages are written to %s",
- qUtf8Printable(m_LogFile.fileName()));
+ log::debug("usvfs log messages are written to {}", m_LogFile.fileName());
}
LogWorker::~LogWorker()
@@ -90,15 +90,16 @@ void LogWorker::exit()
m_QuitRequested = true;
}
-LogLevel logLevel(int level)
+LogLevel toUsvfsLogLevel(log::Levels level)
{
- switch (static_cast<LogLevel>(level)) {
- case LogLevel::Info:
+ switch (level) {
+ case log::Info:
return LogLevel::Info;
- case LogLevel::Warning:
+ case log::Warning:
return LogLevel::Warning;
- case LogLevel::Error:
+ case log::Error:
return LogLevel::Error;
+ case log::Debug: // fall-through
default:
return LogLevel::Debug;
}
@@ -120,20 +121,41 @@ CrashDumpsType crashDumpsType(int type)
UsvfsConnector::UsvfsConnector()
{
- USVFSParameters params;
- LogLevel level = logLevel(Settings::instance().logLevel());
- CrashDumpsType dumpType = crashDumpsType(Settings::instance().crashDumpsType());
+ using namespace std::chrono;
+
+ const auto& s = Settings::instance();
+ const LogLevel logLevel = toUsvfsLogLevel(s.diagnostics().logLevel());
+ const CrashDumpsType dumpType = s.diagnostics().crashDumpsType();
+ const auto delay = duration_cast<milliseconds>(s.diagnostics().spawnDelay());
std::string dumpPath = MOShared::ToString(OrganizerCore::crashDumpsPath(), true);
- USVFSInitParameters(&params, SHMID, false, level, dumpType, dumpPath.c_str());
+
+ usvfsParameters* params = usvfsCreateParameters();
+
+ usvfsSetInstanceName(params, SHMID);
+ usvfsSetDebugMode(params, false);
+ usvfsSetLogLevel(params, logLevel);
+ usvfsSetCrashDumpType(params, dumpType);
+ usvfsSetCrashDumpPath(params, dumpPath.c_str());
+ usvfsSetProcessDelay(params, delay.count());
+
InitLogging(false);
- qDebug("Initializing VFS <%s, %d, %d, %s>", params.instanceName, params.logLevel, params.crashDumpsType, params.crashDumpsPath);
+ log::debug(
+ "initializing usvfs:\n"
+ " . instance: {}\n"
+ " . log: {}\n"
+ " . dump: {} ({})",
+ SHMID,
+ usvfsLogLevelToString(logLevel),
+ dumpPath.c_str(),
+ usvfsCrashDumpTypeToString(dumpType));
- CreateVFS(&params);
+ usvfsCreateVFS(params);
+ usvfsFreeParameters(params);
ClearExecutableBlacklist();
- for (auto exec : Settings::instance().executablesBlacklist().split(";")) {
+ for (auto exec : s.executablesBlacklist().split(";")) {
std::wstring buf = exec.toStdWString();
BlacklistExecutable(buf.data());
}
@@ -159,15 +181,16 @@ UsvfsConnector::~UsvfsConnector()
void UsvfsConnector::updateMapping(const MappingType &mapping)
{
- QProgressDialog progress;
+ QProgressDialog progress(qApp->activeWindow());
progress.setLabelText(tr("Preparing vfs"));
progress.setMaximum(static_cast<int>(mapping.size()));
progress.show();
+
int value = 0;
int files = 0;
int dirs = 0;
- qDebug("Updating VFS mappings...");
+ log::debug("Updating VFS mappings...");
ClearVirtualMappings();
@@ -195,19 +218,27 @@ void UsvfsConnector::updateMapping(const MappingType &mapping)
}
}
- qDebug("VFS mappings updated <linked %d dirs, %d files>", dirs, files);
- /*
- size_t dumpSize = 0;
- CreateVFSDump(nullptr, &dumpSize);
- std::unique_ptr<char[]> buffer(new char[dumpSize]);
- CreateVFSDump(buffer.get(), &dumpSize);
- qDebug(buffer.get());
- */
+ log::debug("VFS mappings updated <linked {} dirs, {} files>", dirs, files);
}
-void UsvfsConnector::updateParams(int logLevel, int crashDumpsType, QString executableBlacklist)
+void UsvfsConnector::updateParams(
+ MOBase::log::Levels logLevel, CrashDumpsType crashDumpsType,
+ const QString& crashDumpsPath, std::chrono::seconds spawnDelay,
+ QString executableBlacklist)
{
- USVFSUpdateParams(::logLevel(logLevel), ::crashDumpsType(crashDumpsType));
+ using namespace std::chrono;
+
+ usvfsParameters* p = usvfsCreateParameters();
+
+ usvfsSetDebugMode(p, FALSE);
+ usvfsSetLogLevel(p, toUsvfsLogLevel(logLevel));
+ usvfsSetCrashDumpType(p, crashDumpsType);
+ usvfsSetCrashDumpPath(p, crashDumpsPath.toStdString().c_str());
+ usvfsSetProcessDelay(p, duration_cast<milliseconds>(spawnDelay).count());
+
+ usvfsUpdateParameters(p);
+ usvfsFreeParameters(p);
+
ClearExecutableBlacklist();
for (auto exec : executableBlacklist.split(";")) {
std::wstring buf = exec.toStdWString();
@@ -227,3 +258,53 @@ void UsvfsConnector::updateForcedLibraries(const QList<MOBase::ExecutableForcedL
}
}
}
+
+
+std::vector<HANDLE> getRunningUSVFSProcesses()
+{
+ std::vector<DWORD> pids;
+
+ {
+ size_t count = 0;
+ DWORD* buffer = nullptr;
+ if (!::GetVFSProcessList2(&count, &buffer)) {
+ log::error("failed to get usvfs process list");
+ return {};
+ }
+
+ if (buffer) {
+ pids.assign(buffer, buffer + count);
+ std::free(buffer);
+ }
+ }
+
+ const auto thisPid = GetCurrentProcessId();
+ std::vector<HANDLE> v;
+
+ const auto rights =
+ PROCESS_QUERY_LIMITED_INFORMATION | // exit code, image name, etc.
+ SYNCHRONIZE | // wait functions
+ PROCESS_SET_QUOTA | PROCESS_TERMINATE; // add to job
+
+ for (auto&& pid : pids) {
+ if (pid == thisPid) {
+ continue; // obviously don't wait for MO process
+ }
+
+ HANDLE handle = ::OpenProcess(rights, FALSE, pid);
+
+ if (handle == INVALID_HANDLE_VALUE) {
+ const auto e = GetLastError();
+
+ log::warn(
+ "failed to open usvfs process {}: {}",
+ pid, formatSystemMessage(e));
+
+ continue;
+ }
+
+ v.push_back(handle);
+ }
+
+ return v;
+}
diff --git a/src/usvfsconnector.h b/src/usvfsconnector.h
index 8a88bde5..5982778b 100644
--- a/src/usvfsconnector.h
+++ b/src/usvfsconnector.h
@@ -29,6 +29,7 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#include <QDebug>
#include <QList>
#include <usvfsparameters.h>
+#include <log.h>
#include "executableinfo.h"
@@ -84,8 +85,14 @@ public:
~UsvfsConnector();
void updateMapping(const MappingType &mapping);
- void updateParams(int logLevel, int crashDumpsType, QString executableBlacklist);
- void updateForcedLibraries(const QList<MOBase::ExecutableForcedLoadSetting> &forcedLibraries);
+
+ void updateParams(
+ MOBase::log::Levels logLevel, CrashDumpsType crashDumpsType,
+ const QString& crashDumpsPath, std::chrono::seconds spawnDelay,
+ QString executableBlacklist);
+
+ void updateForcedLibraries(
+ const QList<MOBase::ExecutableForcedLoadSetting> &forcedLibraries);
private:
@@ -96,4 +103,6 @@ private:
CrashDumpsType crashDumpsType(int type);
+std::vector<HANDLE> getRunningUSVFSProcesses();
+
#endif // USVFSCONNECTOR_H
diff --git a/src/validationprogressdialog.ui b/src/validationprogressdialog.ui
new file mode 100644
index 00000000..1c0cbcb8
--- /dev/null
+++ b/src/validationprogressdialog.ui
@@ -0,0 +1,117 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>ValidationProgressDialog</class>
+ <widget class="QDialog" name="ValidationProgressDialog">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>305</width>
+ <height>93</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>Validating Nexus Connection</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_2">
+ <item>
+ <widget class="QWidget" name="widget" native="true">
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QLabel" name="label">
+ <property name="text">
+ <string>Connecting to Nexus...</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QProgressBar" name="progress">
+ <property name="value">
+ <number>24</number>
+ </property>
+ <property name="textVisible">
+ <bool>false</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="verticalSpacer">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>40</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QWidget" name="widget_2" native="true">
+ <layout class="QHBoxLayout" name="horizontalLayout">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QPushButton" name="cancel">
+ <property name="text">
+ <string>Cancel</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>122</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QPushButton" name="hide">
+ <property name="text">
+ <string>Hide</string>
+ </property>
+ <property name="default">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/src/version.rc b/src/version.rc
index d31c746e..f6d89d2e 100644
--- a/src/version.rc
+++ b/src/version.rc
@@ -3,8 +3,8 @@
// If VS_FF_PRERELEASE is not set, MO labels the build as a release and uses VER_FILEVERSION to determine version number.
// Otherwise, if letters are used in VER_FILEVERSION_STR, uses the full MOBase::VersionInfo parser
// Otherwise, uses the numbers from VER_FILEVERSION and sets the release type as pre-alpha
-#define VER_FILEVERSION 2,2,1
-#define VER_FILEVERSION_STR "2.2.1\0"
+#define VER_FILEVERSION 2,2,2
+#define VER_FILEVERSION_STR "2.2.2\0"
VS_VERSION_INFO VERSIONINFO
FILEVERSION VER_FILEVERSION
diff --git a/src/waitingonclosedialog.cpp b/src/waitingonclosedialog.cpp
deleted file mode 100644
index 565d0a36..00000000
--- a/src/waitingonclosedialog.cpp
+++ /dev/null
@@ -1,71 +0,0 @@
-/*
-Copyright (C) 2012 Sebastian Herbord. All rights reserved.
-
-This file is part of Mod Organizer.
-
-Mod Organizer is free software: you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation, either version 3 of the License, or
-(at your option) any later version.
-
-Mod Organizer is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
-*/
-
-#include "waitingonclosedialog.h"
-#include "ui_waitingonclosedialog.h"
-
-#include <QPoint>
-#include <QResizeEvent>
-#include <QWidget>
-#include <Qt> // for Qt::FramelessWindowHint, etc
-
-WaitingOnCloseDialog::WaitingOnCloseDialog(QWidget *parent)
- : LockedDialogBase(parent,true)
- , ui(new Ui::WaitingOnCloseDialog)
-{
- ui->setupUi(this);
-
- // Supposedly the Qt::CustomizeWindowHint should use a customized window
- // allowing us to select if there is a close button. In practice this doesn't
- // seem to work. We will ignore pressing the close button if unlockByButton == true
- Qt::WindowFlags flags =
- this->windowFlags() | Qt::CustomizeWindowHint | Qt::WindowTitleHint | Qt::WindowMinimizeButtonHint;
- if (m_allowClose)
- flags |= Qt::WindowCloseButtonHint;
- this->setWindowFlags(flags);
-}
-
-WaitingOnCloseDialog::~WaitingOnCloseDialog()
-{
- delete ui;
-}
-
-
-void WaitingOnCloseDialog::setProcessName(const QString &name)
-{
- ui->processLabel->setText(name);
-}
-
-void WaitingOnCloseDialog::on_closeButton_clicked()
-{
- unlock();
-}
-
-void WaitingOnCloseDialog::on_cancelButton_clicked()
-{
- cancel();
- unlock();
-}
-
-void WaitingOnCloseDialog::unlock() {
- LockedDialogBase::unlock();
- ui->label->setText("unlocking may take a few seconds");
- ui->closeButton->setEnabled(false);
- ui->cancelButton->setEnabled(false);
-}
diff --git a/src/waitingonclosedialog.h b/src/waitingonclosedialog.h
deleted file mode 100644
index 6650c390..00000000
--- a/src/waitingonclosedialog.h
+++ /dev/null
@@ -1,56 +0,0 @@
-/*
-Copyright (C) 2012 Sebastian Herbord. All rights reserved.
-
-This file is part of Mod Organizer.
-
-Mod Organizer is free software: you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation, either version 3 of the License, or
-(at your option) any later version.
-
-Mod Organizer is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
-*/
-
-#pragma once
-
-#include "lockeddialogbase.h"
-
-namespace Ui {
- class WaitingOnCloseDialog;
-}
-
-/**
- * Similar to the LockedDialog but used for waiting on running process during
- * a process close request which requries a slightly different dialog.
- **/
-class WaitingOnCloseDialog : public LockedDialogBase
-{
- Q_OBJECT
-
-public:
- explicit WaitingOnCloseDialog(QWidget *parent = 0);
- ~WaitingOnCloseDialog();
-
- bool canceled() const { return m_Canceled; }
-
- void setProcessName(const QString &name) override;
-
-protected:
-
- void unlock() override;
-
-private slots:
-
- void on_closeButton_clicked();
- void on_cancelButton_clicked();
-
-private:
-
- Ui::WaitingOnCloseDialog *ui;
-};
diff --git a/src/waitingonclosedialog.ui b/src/waitingonclosedialog.ui
deleted file mode 100644
index 9c7818e0..00000000
--- a/src/waitingonclosedialog.ui
+++ /dev/null
@@ -1,119 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<ui version="4.0">
- <class>WaitingOnCloseDialog</class>
- <widget class="QDialog" name="WaitingOnCloseDialog">
- <property name="geometry">
- <rect>
- <x>0</x>
- <y>0</y>
- <width>317</width>
- <height>151</height>
- </rect>
- </property>
- <property name="windowTitle">
- <string>Waiting for virtualized processes</string>
- </property>
- <layout class="QVBoxLayout" name="verticalLayout" stretch="1,0,0">
- <item>
- <widget class="QLabel" name="label">
- <property name="toolTip">
- <string>This dialog should disappear automatically if the application/game is done.</string>
- </property>
- <property name="text">
- <string>Virtualized processes are still running, it is prefered to keep MO running until they are finished.</string>
- </property>
- <property name="wordWrap">
- <bool>true</bool>
- </property>
- </widget>
- </item>
- <item>
- <spacer name="verticalSpacer1">
- <property name="orientation">
- <enum>Qt::Vertical</enum>
- </property>
- <property name="sizeHint" stdset="0">
- <size>
- <height>10</height>
- </size>
- </property>
- </spacer>
- </item>
- <item>
- <widget class="QLabel" name="processLabel">
- <property name="font">
- <font>
- <italic>true</italic>
- </font>
- </property>
- <property name="styleSheet">
- <string notr="true">color: grey;</string>
- </property>
- <property name="text">
- <string/>
- </property>
- <property name="alignment">
- <set>Qt::AlignCenter</set>
- </property>
- </widget>
- </item>
- <item>
- <spacer name="verticalSpacer2">
- <property name="orientation">
- <enum>Qt::Vertical</enum>
- </property>
- <property name="sizeHint" stdset="0">
- <size>
- <height>10</height>
- </size>
- </property>
- </spacer>
- </item>
- <item>
- <layout class="QHBoxLayout" name="horizontalLayout" stretch="1,0,0">
- <item>
- <widget class="QPushButton" name="closeButton">
- <property name="text">
- <string>Close Now</string>
- </property>
- </widget>
- </item>
- <item>
- <spacer name="horizontalSpacer_6">
- <property name="orientation">
- <enum>Qt::Horizontal</enum>
- </property>
- <property name="sizeHint" stdset="0">
- <size>
- <width>40</width>
- <height>20</height>
- </size>
- </property>
- </spacer>
- </item>
- <item>
- <widget class="QPushButton" name="cancelButton">
- <property name="text">
- <string>Cancel</string>
- </property>
- </widget>
- </item>
- </layout>
- </item>
- <item>
- <spacer name="verticalSpacer3">
- <property name="orientation">
- <enum>Qt::Vertical</enum>
- </property>
- <property name="sizeHint" stdset="0">
- <size>
- <height>10</height>
- </size>
- </property>
- </spacer>
- </item>
- </layout>
- </widget>
- <resources/>
- <connections/>
-</ui>