diff --git a/.gitignore b/.gitignore index b089495..3800025 100644 --- a/.gitignore +++ b/.gitignore @@ -126,3 +126,5 @@ dmypy.json # Visual Studio Code settings .vscode/ +/.vs +/.idea diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/QtPyHammer/ops/vmf.py b/QtPyHammer/ops/vmf.py index 2d46017..45b2ed1 100644 --- a/QtPyHammer/ops/vmf.py +++ b/QtPyHammer/ops/vmf.py @@ -14,7 +14,7 @@ def __init__(self, parent, vmf_filename): self.parent.viewport.render_manager.add_brushes(*self.brushes) # track changes with CRDT for Undo & Redo # add entities - self.hidden = self._vmf.hidden + #self.hidden = self._vmf.hidden TODO:vmf_tool does not support hidden objects # TODO: apply hide to render_manager # TODO: inform hide related UI if anything is hidden diff --git a/QtPyHammer/ui/__init__.py b/QtPyHammer/ui/__init__.py index 5606c66..1d12a58 100644 --- a/QtPyHammer/ui/__init__.py +++ b/QtPyHammer/ui/__init__.py @@ -3,6 +3,8 @@ from . import user_preferences from . import viewport from . import workspace +from . import popup +from . import compile -__all__ = ["core", "entity", "user_preferences", "viewport", "workspace"] +__all__ = ["core", "entity", "user_preferences", "viewport", "workspace", "popup", "compile"] diff --git a/QtPyHammer/ui/compile.py b/QtPyHammer/ui/compile.py new file mode 100644 index 0000000..6a4f77a --- /dev/null +++ b/QtPyHammer/ui/compile.py @@ -0,0 +1,116 @@ +from PyQt5 import QtCore, QtGui, QtWidgets + +from ..utilities import lang +from ..ui import popup +import subprocess + +class browser(QtWidgets.QDialog): + def __init__(self, parent): + super(browser, self).__init__(parent, QtCore.Qt.Tool) + + self.setWindowTitle("Run Map") + + # Add QLabel widgets + self.box1 = QtWidgets.QLabel("Run BSP:") + self.bsp_combo_box = QtWidgets.QComboBox() + self.bsp_combo_box.setGeometry(200, 150, 120, 40) + self.bsp_combo_box.addItem(lang.langNo()) + self.bsp_combo_box.addItem(lang.langNormal()) + self.bsp_combo_box.addItem("Only Entities") + self.bsp_combo_box.setCurrentIndex(self.bsp_combo_box.findText(lang.langNormal())) + + self.box2 = QtWidgets.QLabel("Run VIS:") + self.vis_combo_box = QtWidgets.QComboBox() + self.vis_combo_box.setGeometry(200, 150, 120, 40) + self.vis_combo_box.addItem(lang.langNo()) + self.vis_combo_box.addItem(lang.langNormal()) + self.vis_combo_box.addItem(lang.langFast()) + self.vis_combo_box.setCurrentIndex(self.vis_combo_box.findText(lang.langNormal())) + + self.box3 = QtWidgets.QLabel("Run RAD:") + self.rad_combo_box = QtWidgets.QComboBox() + self.rad_combo_box.setGeometry(200, 150, 120, 40) + self.rad_combo_box.addItem(lang.langNo()) + self.rad_combo_box.addItem(lang.langNormal()) + self.rad_combo_box.addItem(lang.langFast()) + self.rad_combo_box.setCurrentIndex(self.rad_combo_box.findText(lang.langFast())) + + self.box4 = QtWidgets.QLabel("Path to VMF") + self.textbox = QtWidgets.QLineEdit(self) + self.textbox.resize(280,40) + + # Layout setup + base_layout = QtWidgets.QVBoxLayout() + base_layout.addWidget(self.box1) + base_layout.addWidget(self.bsp_combo_box) + base_layout.addWidget(self.box2) + base_layout.addWidget(self.vis_combo_box) + base_layout.addWidget(self.box3) + base_layout.addWidget(self.rad_combo_box) + base_layout.addWidget(self.box4) + base_layout.addWidget(self.textbox) + bottom_row = QtWidgets.QHBoxLayout() + bottom_row.addStretch(1) + cancel_button = QtWidgets.QPushButton("Cancel") + cancel_button.clicked.connect(self.reject) + bottom_row.addWidget(cancel_button) + ok_button = QtWidgets.QPushButton(lang.langOk()) + ok_button.clicked.connect(self.on_ok_clicked) + ok_button.setDefault(True) + bottom_row.addWidget(ok_button) + base_layout.addLayout(bottom_row) + self.setLayout(base_layout) + + # Resize the dialog to fit the text + self.adjustSize() + + def on_ok_clicked(self): + preferences = QtWidgets.QApplication.instance().game_config + vbsp_path = preferences.value("Hammer/BSP", r"C:/Program Files (x86)/Steam/steamapps/common/Team Fortress 2/bin/vbsp.exe") + vvis_path = preferences.value("Hammer/Vis", r"C:/Program Files (x86)/Steam/steamapps/common/Team Fortress 2/bin/vvis.exe") + vrad_path = preferences.value("Hammer/Light", r"C:/Program Files (x86)/Steam/steamapps/common/Team Fortress 2/bin/vrad.exe") + + game_path = preferences.value("General/GameDir", r"C:\Program Files (x86)\Steam\steamapps\common\Team Fortress 2\tf") + file_path = self.textbox.text() + + if not file_path: + no_text_popup = popup.browser(parent=self, popuptext="Error", msgtext="File path cannot be empty") + no_text_popup.show() + return + bsp_index = self.bsp_combo_box.currentIndex() + vis_index = self.vis_combo_box.currentIndex() + rad_index = self.rad_combo_box.currentIndex() + + vbsp_arguments = ['-game', game_path] + vvis_arguments = ['-game', game_path] + vrad_arguments = ['-game', game_path] + + if bsp_index == 0: + vbsp_arguments += ['-leaktest', file_path] + elif bsp_index == 1: + vbsp_arguments += ['-leaktest', file_path] + elif bsp_index == 2: + vbsp_arguments += ['-leaktest', '-onlyents', file_path] + + if vis_index == 0: + vvis_arguments += [file_path] + elif vis_index == 1: + vvis_arguments += [file_path] + elif vis_index == 2: + vvis_arguments += ['-fast', file_path] + + if rad_index == 0: + vrad_arguments += [file_path] + elif rad_index == 1: + vrad_arguments += [file_path] + elif rad_index == 2: + vrad_arguments += ['-fast', file_path] + + vbsp_command = [vbsp_path] + vbsp_arguments + vvis_command = [vvis_path] + vvis_arguments + vrad_command = [vrad_path] + vrad_arguments + + subprocess.run(vbsp_command, check=True) + subprocess.run(vvis_command, check=True) + subprocess.run(vrad_command, check=True) + self.accept() diff --git a/QtPyHammer/ui/core.py b/QtPyHammer/ui/core.py index 62dc60c..b5ba7bf 100644 --- a/QtPyHammer/ui/core.py +++ b/QtPyHammer/ui/core.py @@ -2,17 +2,20 @@ import os from PyQt5 import QtCore, QtGui, QtWidgets +from PyQt5.QtGui import QIcon from .. import ops -from ..ui import entity +from ..ui import entity, popup, texture_browser, compile, properties from ..ui import workspace +from ..utilities import lang class MainWindow(QtWidgets.QMainWindow): def __init__(self, parent=None): super(QtWidgets.QMainWindow, self).__init__(parent) global current_dir - self.setWindowTitle("QtPyHammer") + self.setWindowIcon(QtGui.QIcon('HammerLogo.png')) + self.setWindowTitle("QtPyHammer - Fork") self.setMinimumSize(640, 480) self.setTabPosition(QtCore.Qt.TopDockWidgetArea, QtWidgets.QTabWidget.North) self.tabs = QtWidgets.QTabWidget() @@ -29,18 +32,30 @@ def __init__(self, parent=None): self.actions = {} # ^ {"identifier": action} self.main_menu = QtWidgets.QMenuBar() - file_menu = self.main_menu.addMenu("&File") + file_menu = self.main_menu.addMenu(lang.langFile()) self.actions["File/New"] = file_menu.addAction("&New") - def new_file(): ops.new_file(self) + + def new_file(): + ops.new_file(self) + self.actions["File/New"].triggered.connect(new_file) self.actions["File/Open"] = file_menu.addAction("&Open") - def open_files(): ops.open_files(self, self.map_browser) + + def open_files(): + ops.open_files(self, self.map_browser) + self.actions["File/Open"].triggered.connect(open_files) self.actions["File/Save"] = file_menu.addAction("&Save") - def save_file(): ops.save_file(self, self.map_browser) + + def save_file(): + ops.save_file(self, self.map_browser) + self.actions["File/Save"].triggered.connect(save_file) self.actions["File/Save As"] = file_menu.addAction("Save &As") - def save_file_as(): ops.save_file_as(self, self.map_browser) + + def save_file_as(): + ops.save_file_as(self, self.map_browser) + self.actions["File/Save As"].triggered.connect(save_file_as) file_menu.addSeparator() # self.import_menu = file_menu.addMenu("Import") @@ -52,21 +67,21 @@ def save_file_as(): ops.save_file_as(self, self.map_browser) # export_menu.addAction(".smd") file_menu.addSeparator() self.actions["File/Options"] = file_menu.addAction("&Options") - self.actions["File/Options"].setEnabled(False) - # self.actions["File/Options"].triggered.connect(ui.settings) + properties_menu = properties.browser(parent=self) + self.actions["File/Options"].triggered.connect(properties_menu.show) file_menu.addSeparator() - self.actions["File/Compile"] = file_menu.addAction("Compile") - self.actions["File/Compile"].setEnabled(False) - # self.actions["File/Compile"].triggered.connect(ui.compile) + self.actions["File/Compile"] = file_menu.addAction(lang.langCompile()) + compile_menu = compile.browser(parent=self) + self.actions["File/Compile"].triggered.connect(compile_menu.show) file_menu.addSeparator() - self.actions["File/Exit"] = file_menu.addAction("Exit") + self.actions["File/Exit"] = file_menu.addAction(lang.langExit()) self.actions["File/Exit"].triggered.connect(QtCore.QCoreApplication.quit) - edit_menu = self.main_menu.addMenu("&Edit") - self.actions["Edit/Undo"] = edit_menu.addAction("Undo") + edit_menu = self.main_menu.addMenu(lang.langEdit()) + self.actions["Edit/Undo"] = edit_menu.addAction(lang.langUndo()) self.actions["Edit/Undo"].setEnabled(False) # self.actions["Edit/Undo"].triggered.connect( # edit timeline - self.actions["Edit/Redo"] = edit_menu.addAction("Redo") + self.actions["Edit/Redo"] = edit_menu.addAction(lang.langRedo()) self.actions["Edit/Redo"].setEnabled(False) # self.actions["Edit/Redo"].triggered.connect( # edit timeline self.actions["Edit/History"] = edit_menu.addMenu("&History...") @@ -100,7 +115,7 @@ def save_file_as(): ops.save_file_as(self, self.map_browser) self.actions["Edit/Properties"].setEnabled(False) # self.actions["Edit/Properties"].triggered.connect( - tools_menu = self.main_menu.addMenu("&Tools") + tools_menu = self.main_menu.addMenu(lang.langTools()) self.actions["Tools/Group"] = tools_menu.addAction("&Group") self.actions["Tools/Group"].setEnabled(False) # self.actions["Tools/Group"].triggered.connect( @@ -113,8 +128,8 @@ def save_file_as(): ops.save_file_as(self, self.map_browser) ent_browser = entity.browser(parent=self) self.actions["Tools/Brush to Entity"].triggered.connect(ent_browser.show) except Exception as exc: - # log the full exception for debug - print("Failed to load .fgds!") # use the builtin logger module + error_popup = popup.browser(parent=self, popuptext="Error", msgtext="Failed to load .fgds!") + self.actions["Tools/Brush to Entity"].triggered.connect(error_popup.show) self.actions["Tools/Brush to Entity"].setEnabled(False) raise exc self.actions["Tools/Entity to Brush"] = tools_menu.addAction("&Move to World") @@ -256,23 +271,27 @@ def save_file_as(): ops.save_file_as(self, self.map_browser) # self.actions["Help/Offline"].triggered.connect(ui. help_menu.addSeparator() self.actions["Help/About QPH"] = help_menu.addAction("About QtPyHammer") - self.actions["Help/About QPH"].triggered.connect(lambda: open_url(QtCore.QUrl( - "https://github.com/snake-biscuits/QtPyHammer/wiki"))) + about_popup = popup.browser(parent=self, popuptext="About", + msgtext="A Python alternative to Valve Hammer Editor 4.x, forked from QtPyHammer\n\nVersion: v0.0.5forked") + self.actions["Help/About QPH"].triggered.connect(about_popup.show) self.actions["Help/About Qt"] = help_menu.addAction("About Qt") - self.actions["Help/About Qt"].setEnabled(False) + self.actions["Help/About Qt"].triggered.connect(lambda: open_url(QtCore.QUrl( + "https://github.com/spyder-ide/qtpy"))) # self.actions["Help/About Qt"].triggered.connect(ui. #QDialog self.actions["Help/License"] = help_menu.addAction("License") - self.actions["Help/License"].setEnabled(False) + self.actions["Help/License"].triggered.connect(lambda: open_url(QtCore.QUrl( + "https://github.com/strubium/QtPyHammer/blob/master/LICENSE"))) # self.actions["Help/License"].triggered.connect(ui. #QDialog self.actions["Help/Contributors"] = help_menu.addAction("Contributors") - self.actions["Help/Contributors"].setEnabled(False) - # self.actions["Help/Contributors"].triggered.connect(ui. #QDialog + self.actions["Help/Contributors"].triggered.connect( + lambda: open_url(QtCore.QUrl("https://github.com/QtPyHammer-devs/QtPyHammer/graphs/contributors"))) + self.actions["Help/QPH Wiki"] = help_menu.addAction("QtPyHammer Wiki") + self.actions["Help/QPH Wiki"].triggered.connect(lambda: open_url(QtCore.QUrl( + "https://github.com/snake-biscuits/QtPyHammer/wiki"))) help_menu.addSeparator() self.actions["Help/VDC"] = help_menu.addAction("Valve Developer Community") self.actions["Help/VDC"].triggered.connect(lambda: open_url(QtCore.QUrl( - "https://developer.valvesoftware.com/wiki/Main_Page"))) - self.actions["Help/TF2Maps"] = help_menu.addAction("TF2Maps.net") - self.actions["Help/TF2Maps"].triggered.connect(lambda: open_url(QtCore.QUrl("https://tf2maps.net"))) + "https://developer.valvesoftware.com/wiki/Main_Page"))) # attach all actions to hotkeys app = QtWidgets.QApplication.instance() @@ -288,27 +307,81 @@ def save_file_as(): ops.save_file_as(self, self.map_browser) self.setMenuBar(self.main_menu) # TOOLBARS - # key_tools = QtWidgets.QToolBar("Tools 1") - # key_tools.setMovable(False) - # button_1 = QtWidgets.QToolButton() # need icons (.png) - # button_1.setToolTip("Toggle 2D grid visibility") - # key_tools.addWidget(button_1) - # button_2 = QtWidgets.QToolButton() - # button_2.setToolTip("Toggle 3D grid visibility") - # key_tools.addWidget(button_2) - # button_3 = QtWidgets.QToolButton() - # button_3.setToolTip("Grid scale - [") - # key_tools.addWidget(button_3) - # button_3 = QtWidgets.QToolButton() - # button_3.setDefaultAction(...) # shortcut "]" - # key_tools.addWidget(button_3) - # key_tools.addSeparator() + key_tools = QtWidgets.QToolBar("Tools") + key_tools.setMovable(True) + button_1 = QtWidgets.QToolButton() # need icons (.png) + button_1.setToolTip("Toggle 2D grid visibility") + button_1.setIcon(QIcon("icons/2dHammerIcon")) + button_1.setEnabled(False) + key_tools.addWidget(button_1) + button_2 = QtWidgets.QToolButton() + button_2.setToolTip("Toggle 3D grid visibility") + button_2.setIcon(QIcon("icons/3dHammerIcon")) + button_2.setEnabled(False) + key_tools.addWidget(button_2) + button_3 = QtWidgets.QToolButton() + button_3.setToolTip("Smaller Grid") + button_3.setEnabled(False) + key_tools.addWidget(button_3) + button_4 = QtWidgets.QToolButton() + button_4.setToolTip("Larger Grid") + button_4.setIcon(QIcon("icons/LargeGridIcon")) + button_4.setEnabled(False) + key_tools.addWidget(button_4) + key_tools.addSeparator() + button_5 = QtWidgets.QToolButton() + button_5.setToolTip("Load Window State") + button_5.setEnabled(False) + key_tools.addWidget(button_5) + button_6 = QtWidgets.QToolButton() + button_6.setToolTip("Save Window State") + button_6.setEnabled(False) + key_tools.addWidget(button_6) + key_tools.addSeparator() + button_7 = QtWidgets.QToolButton() + button_7.setToolTip("Undo") + button_7.setEnabled(False) + key_tools.addWidget(button_7) + button_8 = QtWidgets.QToolButton() + button_8.setToolTip("Redo") + button_8.setEnabled(False) + key_tools.addWidget(button_8) + key_tools.addSeparator() - # self.addToolBar(QtCore.Qt.TopToolBarArea, key_tools) + self.addToolBar(QtCore.Qt.TopToolBarArea, key_tools) # undo redo | carve | group ungroup ignore | hide unhide alt-hide | # cut copy paste | cordon | TL | DD 3D DW DA | # compile helpers 2D_models fade CM prop_detail NO_DRAW + right_toolbar = QtWidgets.QToolBar("Sidebar") + right_toolbar.setFixedWidth(115) + label_1 = QtWidgets.QLabel("Select:") + right_toolbar.addWidget(label_1) + right_toolbar.setMovable(True) + button_1 = QtWidgets.QPushButton("Groups") + button_1.setFixedSize(100, 25) + button_1.setEnabled(False) + right_toolbar.addWidget(button_1) + button_2 = QtWidgets.QPushButton("Objects") + button_2.setFixedSize(100, 25) + button_2.setEnabled(False) + right_toolbar.addWidget(button_2) + button_3 = QtWidgets.QPushButton("Solids") + button_3.setFixedSize(100, 25) + button_3.setEnabled(False) + right_toolbar.addWidget(button_3) + right_toolbar.addSeparator() + + label_2 = QtWidgets.QLabel("Texture Selection:") + right_toolbar.addWidget(label_2) + button_4 = QtWidgets.QPushButton("Browse") + button_4.setFixedSize(100, 25) + texture_popup = texture_browser.TextureBrowser(parent=self) + button_4.clicked.connect(texture_popup.show) + right_toolbar.addWidget(button_4) + + self.addToolBar(QtCore.Qt.RightToolBarArea, right_toolbar) + def open(self, filename): # allows loading via drag & drop raw_filename, extension = os.path.splitext(filename) short_filename = os.path.basename(filename) diff --git a/QtPyHammer/ui/entity.py b/QtPyHammer/ui/entity.py index 7c44a78..623ed85 100644 --- a/QtPyHammer/ui/entity.py +++ b/QtPyHammer/ui/entity.py @@ -16,7 +16,8 @@ def __init__(self, parent): self.setGeometry(780, 220, 360, 640) app = QtWidgets.QApplication.instance() if len(app.fgd.entities) == 0: - raise RuntimeError("No entites to browse!") + error_popup = popup.browser(parent=self, popuptext="Error", msgtext="No entites to browse!") + error_popup.show() def is_point_or_solid(e): return e.class_type in ("PointClass", "SolidClass") @@ -112,7 +113,7 @@ def load_entity(self, index): # ADD SmartEdit toggle & tooltips outputs = [*filter(lambda o: isinstance(o, valvefgd.parser.FgdEntityOutput), entity.properties)] # split properly in some version of valvefgd (prob 1.0.0 but it's broken?) if len(inputs) > 0 or len(outputs) > 0: # OR ANY inputs recieved - logic_widget = QtWidgets.QWidget() # <- make it's own class + logic_widget = QtWidgets.QWidget() # <- make it its own class logic_widget.setLayout(QtWidgets.QVBoxLayout()) logic_widget.layout().addWidget(QtWidgets.QLabel("Inputs")) logic_widget.layout().addWidget(QtWidgets.QLabel("Outputs")) diff --git a/QtPyHammer/ui/popup.py b/QtPyHammer/ui/popup.py new file mode 100644 index 0000000..72ecb55 --- /dev/null +++ b/QtPyHammer/ui/popup.py @@ -0,0 +1,30 @@ +from PyQt5 import QtCore, QtGui, QtWidgets +from ..utilities import lang + + +class browser(QtWidgets.QDialog): + def __init__(self, parent, popuptext="Error", msgtext="Something's wrong, but we don't know what"): + super(browser, self).__init__(parent, QtCore.Qt.Tool) + self.setWindowTitle(popuptext) + + # Add QLabel to display text + self.text_label = QtWidgets.QLabel(msgtext) + self.text_label.setAlignment(QtCore.Qt.AlignCenter) # Center align text + self.text_label.setWordWrap(True) # Enable word wrap + self.text_label.setMargin(15) # Add margin to the label + self.text_label.setSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Minimum) + + # Layout setup + base_layout = QtWidgets.QVBoxLayout() + base_layout.addWidget(self.text_label) + bottom_row = QtWidgets.QHBoxLayout() + bottom_row.addStretch(1) + ok_button = QtWidgets.QPushButton(lang.langOk()) + ok_button.clicked.connect(self.accept) + ok_button.setDefault(True) + bottom_row.addWidget(ok_button) + base_layout.addLayout(bottom_row) + self.setLayout(base_layout) + + # Resize the dialog to fit the text + self.adjustSize() diff --git a/QtPyHammer/ui/properties.py b/QtPyHammer/ui/properties.py new file mode 100644 index 0000000..e9bf0cf --- /dev/null +++ b/QtPyHammer/ui/properties.py @@ -0,0 +1,49 @@ +from PyQt5 import QtCore, QtGui, QtWidgets +from ..utilities import lang + + +class browser(QtWidgets.QDialog): + def __init__(self, parent): + super(browser, self).__init__(parent, QtCore.Qt.Tool) + self.setWindowTitle("QtPyHammer Properties - WIP") + + self.box1 = QtWidgets.QLabel("Render Mode:") + self.render_mod_combo_box = QtWidgets.QComboBox() + self.render_mod_combo_box.setGeometry(200, 150, 120, 40) + self.render_mod_combo_box.addItem("Flat") + self.render_mod_combo_box.addItem("Textured") + self.render_mod_combo_box.addItem("Wireframe") + self.render_mod_combo_box.setCurrentIndex(self.render_mod_combo_box.findText("Textured")) + + self.box2 = QtWidgets.QLabel("Language:") + self.lang_combo_box = QtWidgets.QComboBox() + self.lang_combo_box.setGeometry(200, 150, 120, 40) + self.lang_combo_box.addItem("English") + self.lang_combo_box.addItem("Spanish") + self.lang_combo_box.addItem("German") + self.lang_combo_box.addItem("Italian") + self.lang_combo_box.addItem("Russian") + self.lang_combo_box.setCurrentIndex(self.lang_combo_box.findText("English")) + + # Layout setup + base_layout = QtWidgets.QVBoxLayout() + base_layout.addWidget(self.box1) + base_layout.addWidget(self.render_mod_combo_box) + base_layout.addWidget(self.box2) + base_layout.addWidget(self.lang_combo_box) + bottom_row = QtWidgets.QHBoxLayout() + bottom_row.addStretch(1) + ok_button = QtWidgets.QPushButton(lang.langOk()) + ok_button.clicked.connect(self.on_ok_clicked) + ok_button.setDefault(True) + bottom_row.addWidget(ok_button) + base_layout.addLayout(bottom_row) + self.setLayout(base_layout) + + # Resize the dialog to fit the text + self.adjustSize() + + def on_ok_clicked(self): + # render_mod_index = self.render_mod_combo_box.currentIndex() + # lang.setLanguage(self.lang_combo_box.currentText()) + self.accept() diff --git a/QtPyHammer/ui/texture_browser.py b/QtPyHammer/ui/texture_browser.py new file mode 100644 index 0000000..14cb2b6 --- /dev/null +++ b/QtPyHammer/ui/texture_browser.py @@ -0,0 +1,213 @@ +import sys + +from PyQt5 import QtCore, QtGui, QtWidgets + +from ..ui import popup + + +class TextureBrowser(QtWidgets.QDialog): + # https://doc.qt.io/qt-5/qdialog.html + def __init__(self, parent=None): + super().__init__(parent) + # pick a layout for the core widget + # top has a scrolling page of texture thumbnails + # bottom has filters, self.searchbar, OK & Cancel buttons + + main_layout = QtWidgets.QVBoxLayout() + self.setWindowTitle("Texture Browser") + self.setGeometry(780, 220, 360, 640) + # now this has scroll bar, but it doesn't have flow layout + scroll = QtWidgets.QScrollArea() + groupbox = QtWidgets.QGroupBox("Textures") + flow_layout = FlowLayout(margin=10) + + container = QtWidgets.QWidget() + container_layout = QtWidgets.QVBoxLayout() + + # configure flow layout + flow_layout.heightChanged.connect(container.setMinimumHeight) + for i in range(400): + self.addTextureSquare(flow_layout) + + # set configured layout to the groupbox + groupbox.setLayout(flow_layout) + + container_layout.addWidget(groupbox) + container_layout.addStretch() + container.setLayout(container_layout) + + # configure scrollarea + scroll.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn) + + # set configured groupbox as widget of the scrollarea + scroll.setWidget(container) + scroll.setWidgetResizable(True) + + # finally add scrollarea widget to the outer QVBoxLayout + main_layout.addWidget(scroll) + main_layout.addWidget(QtWidgets.QLabel("Search Options")) + # main_layout.addWidget(QtWidgets.QLabel("Max:")) + # main_layout.addWidget(QtWidgets.QLineEdit(self).setPlaceholderText("400").resize(280,40)) + + # list of checkboxes? + # colour-range (hue) slider (.vtf reflectivity value) + + self.searchbar = QtWidgets.QLineEdit() + main_layout.addWidget(self.searchbar) + + search_button = QtWidgets.QPushButton("Search") + search_button.clicked.connect(lambda: self.search(self.searchbar.text())) + search_button.setDefault(1) + + main_layout.addWidget(search_button) + + buttonbox = QtWidgets.QDialogButtonBox + buttons = buttonbox(buttonbox.Ok | buttonbox.Cancel) + buttons.accepted.connect(self.accept) + buttons.rejected.connect(self.reject) + main_layout.addWidget(buttons) + self.setLayout(main_layout) + + def addTextureSquare(self, layout): + image_label = QtWidgets.QLabel() + # Placeholder Image + magenta = b"\xFF\x00\xFF" + black = b"\x00\x00\x00" + data = magenta + black + black + magenta + + # Parse data into image + image = QtGui.QImage(data, 2, 2, QtGui.QImage.Format_RGB888) + image.setDevicePixelRatio(1 / 32) # scale *32 + image_label.setPixmap(QtGui.QPixmap.fromImage(image)) + + layout.addWidget(image_label) + + def search(self, keyword): + if not keyword: + error_popup = popup.browser(parent=self, popuptext="Error", msgtext="Search text cannot be empty") + error_popup.show() + else: + search_popup = popup.browser(parent=self, popuptext="Notification", msgtext=f"Searching for: {keyword}!") + search_popup.show() + # TODO: doesnt filter yet + + +class FlowLayout(QtWidgets.QLayout): + """A ``QLayout`` that aranges its child widgets horizontally and + vertically. + + If enough horizontal space is available, it looks like an ``HBoxLayout``, + but if enough space is lacking, it automatically wraps its children into + multiple rows.""" + heightChanged = QtCore.pyqtSignal(int) + + def __init__(self, parent=None, margin=0, spacing=-1): + super().__init__(parent) + if parent is not None: + self.setContentsMargins(margin, margin, margin, margin) + self.setSpacing(spacing) + + self._item_list = [] + + def __del__(self): + while self.count(): + self.takeAt(0) + + def addItem(self, item): + self._item_list.append(item) + + def addSpacing(self, size): + self.addItem(QtWidgets.QSpacerItem(size, 0, QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Minimum)) + + def count(self): + return len(self._item_list) + + def itemAt(self, index): + if 0 <= index < len(self._item_list): + return self._item_list[index] + return None + + def takeAt(self, index): + if 0 <= index < len(self._item_list): + return self._item_list.pop(index) + return None + + @staticmethod + def expandingDirections(): + return QtCore.Qt.Orientations(QtCore.Qt.Orientation(0)) + + def hasHeightForWidth(self): + return True + + def heightForWidth(self, width): + height = self._do_layout(QtCore.QRect(0, 0, width, 0), True) + return height + + def setGeometry(self, rect): + super().setGeometry(rect) + self._do_layout(rect, False) + + def sizeHint(self): + return self.minimumSize() + + def minimumSize(self): + size = QtCore.QSize() + + for item in self._item_list: + minsize = item.minimumSize() + extent = item.geometry().bottomRight() + size = size.expandedTo(QtCore.QSize(minsize.width(), extent.y())) + + margin = self.contentsMargins().left() + size += QtCore.QSize(2 * margin, 2 * margin) + return size + + def _do_layout(self, rect, test_only=False): + m = self.contentsMargins() + effective_rect = rect.adjusted(+m.left(), +m.top(), -m.right(), -m.bottom()) + x = effective_rect.x() + y = effective_rect.y() + line_height = 0 + + for item in self._item_list: + wid = item.widget() + + space_x = self.spacing() + space_y = self.spacing() + if wid is not None: + space_x += wid.style().layoutSpacing(QtWidgets.QSizePolicy.PushButton, + QtWidgets.QSizePolicy.PushButton, QtCore.Qt.Horizontal) + space_y += wid.style().layoutSpacing(QtWidgets.QSizePolicy.PushButton, + QtWidgets.QSizePolicy.PushButton, QtCore.Qt.Vertical) + + next_x = x + item.sizeHint().width() + space_x + if next_x - space_x > effective_rect.right() and line_height > 0: + x = effective_rect.x() + y = y + line_height + space_y + next_x = x + item.sizeHint().width() + space_x + line_height = 0 + + if not test_only: + item.setGeometry(QtCore.QRect(QtCore.QPoint(x, y), item.sizeHint())) + + x = next_x + line_height = max(line_height, item.sizeHint().height()) + + new_height = y + line_height - rect.y() + self.heightChanged.emit(new_height) + return new_height + + +if __name__ == "__main__": + def except_hook(cls, exception, traceback): + """Get tracebacks for python called by Qt functions & classes""" + sys.__excepthook__(cls, exception, traceback) + + + sys.excepthook = except_hook + app = QtWidgets.QApplication(sys.argv) + browser = TextureBrowser() + browser.setGeometry(128, 64, 576, 576) + browser.show() + + app.exec_() diff --git a/QtPyHammer/ui/viewport.py b/QtPyHammer/ui/viewport.py index 3dda92f..536187e 100644 --- a/QtPyHammer/ui/viewport.py +++ b/QtPyHammer/ui/viewport.py @@ -72,10 +72,10 @@ def initializeGL(self): app = QtWidgets.QApplication.instance() shader_folder = os.path.join(app.folder, f"shaders/{self.shader_version}/") self.render_manager.initialise(shader_folder) - self.set_view_mode("flat") # sets shaders & GL state + self.set_view_mode("textured") # sets shaders & GL state self.timer.start() - # calling the slot by it's name creates a QVariant Error + # calling the slot by its name creates a QVariant Error # which for some reason does not trace correctly @QtCore.pyqtSlot(str, name="setViewMode") # connected to UI def set_view_mode(self, view_mode): # C++: void setViewMode(QString) @@ -130,23 +130,23 @@ def resizeGL(self, width, height): # Qt Signals def do_raycast(self, click_x, click_y): - camera_right = vector.vec3(x=1).rotate(*-self.camera.rotation) - camera_up = vector.vec3(z=1).rotate(*-self.camera.rotation) - camera_forward = vector.vec3(y=1).rotate(*-self.camera.rotation) + camera_right = vector.vec3(x=1).rotate(1) + camera_up = vector.vec3(z=1).rotate(1) + camera_forward = vector.vec3(y=1).rotate(1) width, height = self.width(), self.height() x_offset = camera_right * ((click_x * 2 - width) / width) x_offset *= width / height # aspect ratio y_offset = camera_up * ((click_y * 2 - height) / height) - fov_scalar = math.tan(math.radians(self.render_manager.field_of_view / 2)) + fov_scalar = math.tan(math.radians(90 / 2)) x_offset *= fov_scalar y_offset *= fov_scalar - ray_origin = self.camera.position + ray_origin = camera.freecam.position ray_direction = camera_forward + x_offset + y_offset return ray_origin, ray_direction # Rebound Qt Methods def keyPressEvent(self, event): # not registering arrow keys? - # BUG? auto repeat can "give the camera velocity" by jamming a key down virtually? + # BUG? auto-repeat can "give the camera velocity" by jamming a key down virtually? # ^ obsered once by @snake-biscuits self.keys.add(event.key()) @@ -178,7 +178,6 @@ def mouseReleaseEvent(self, event): x = self.width() / 2 y = self.height() / 2 ray_origin, ray_direction = self.do_raycast(x, self.height() - y) - self.raycast.emit(ray_origin, ray_direction) super(MapViewport3D, self).mouseReleaseEvent(event) def wheelEvent(self, event): diff --git a/QtPyHammer/ui/workspace.py b/QtPyHammer/ui/workspace.py index 1aa30dd..7bf589b 100644 --- a/QtPyHammer/ui/workspace.py +++ b/QtPyHammer/ui/workspace.py @@ -1,11 +1,10 @@ """QtPyHammer Workspace that holds and manages an open .vmf file""" import enum - -from PyQt5 import QtWidgets - -from . import viewport -from ..ops.vmf import VmfInterface +from PyQt5 import QtWidgets, QtCore +from PyQt5.QtGui import QMouseEvent +from . import viewport, popup from ..utilities import raycast +from ..ops.vmf import VmfInterface class SELECTION_MODE(enum.Enum): @@ -28,7 +27,6 @@ def __init__(self, vmf_path, new=True, parent=None): layout = QtWidgets.QVBoxLayout() # holds the viewport # ^ 2 QSplitter(s) will be used for quad viewports self.viewport = viewport.MapViewport3D(self) - self.viewport.raycast.connect(self.select) # self.viewport.setViewMode.connect(...) self.viewport.setFocus() # not working as intended layout.addWidget(self.viewport) @@ -39,13 +37,26 @@ def __init__(self, vmf_path, new=True, parent=None): # ^ define the VmfInterface last so it can connect to everything # undo & redo is tied directly to the VmfInterface + def mousePressEvent(self, event: QMouseEvent): + """Override the mouse press event to handle selection""" + if event.button() == QtCore.Qt.LeftButton: + # Convert the mouse position to normalized device coordinates + ndc_x = (2.0 * event.x()) / self.viewport.width() - 1.0 + ndc_y = 1.0 - (2.0 * event.y()) / self.viewport.height() + + # Use a raycasting method to find the selection + ray_origin, ray_direction = viewport.MapViewport3D.do_raycast(self, ndc_x, ndc_y) + self.select(ray_origin, ray_direction) + def select(self, ray_origin, ray_direction): """Get the object hit by ray""" ray_length = self.viewport.render_manager.draw_distance ray = raycast.Ray(ray_origin, ray_direction, ray_length) - selection = raycast.raycast(ray, self.map_file) - self.map_file.selection.add(selection) - # TODO: highlight selection in renderer + objects = self.map_file.get_objects() # Access objects from VmfInterface + selection = raycast(ray, objects) + if selection: + self.selection.add(selection) + # TODO: highlight selection in renderer def save_to_file(self): print(f"Saving {self.filename}... ", end="") @@ -57,9 +68,11 @@ def save_to_file(self): # - hidden state of objects (visgroups included) self.map_file.save(self.filename) except Exception as exc: - print() + error_popup = popup.browser(parent=self, popuptext="Error", msgtext="Error when saving") + error_popup.show() raise exc - print("Saved!") + saved_popup = popup.browser(parent=self, popuptext="Status", msgtext="Saved") + saved_popup.show() self.never_saved = False def close(self): diff --git a/QtPyHammer/utilities/lang.py b/QtPyHammer/utilities/lang.py new file mode 100644 index 0000000..57db017 --- /dev/null +++ b/QtPyHammer/utilities/lang.py @@ -0,0 +1,139 @@ + +usingLanguage = "English" # Default language + +def setLanguage(language): + global usingLanguage + usingLanguage = language.lower() + +def langFile(language=usingLanguage): + match(language): + case "english": + return "File" + case "spanish": + return "Archivo" + case "russian": + return "Файл" + case _: + return "File" + +def langEdit(language=usingLanguage): + match(language): + case "english": + return "Edit" + case "spanish": + return "Editar" + case "russian": + return "Редактировать" + case _: + return "Edit" + +def langTools(language=usingLanguage): + match(language): + case "english": + return "Tools" + case "spanish": + return "Herramientas" + case "russian": + return "Инструменты" + case _: + return "Tools" + +def langOk(language=usingLanguage): + match(language): + case "english": + return "Ok" + case "spanish": + return "De acuerdo" + case "russian": + return "Хорошо" + case _: + return "Ok" + +def langUndo(language=usingLanguage): + match(language): + case "english": + return "Undo" + case "french": + return "Annuler" + case "spanish": + return "Deshacer" + case "german": + return "Rückgängig machen" + case "italian": + return "Annulla" + case "russian": + return "Отменить" + case _: + return "Undo" + +def langRedo(language=usingLanguage): + match(language): + case "english": + return "Redo" + case "spanish": + return "Rehacer" + case "russian": + return "Повторить" + case _: + return "Redo" + +def langCompile(language=usingLanguage): + match(language): + case "english": + return "Compile" + case "spanish": + return "Compilar" + case "russian": + return "Скомпилировать" + case _: + return "Compile" + +def langExit(language=usingLanguage): + match(language): + case "english": + return "Exit" + case "french": + return "Sortir" + case "spanish": + return "Salida" + case "russian": + return "Выход" + case _: + return "Exit" + +def langNo(language=usingLanguage): + match(language): + case "english": + return "No" + case "french": + return "Non" + case "russian": + return "Нет" + case _: + return "No" + +def langNormal(language=usingLanguage): + match(language): + case "english": + return "Normal" + case "french": + return "Normale" + case "spanish": + return "Normala" + case "russian": + return "Нормальный" + case _: + return "Normal" + +def langFast(language=usingLanguage): + match(language): + case "english": + return "Fast" + case "french": + return "Rapide" + case "spanish": + return "Rapido" + case "russian": + return "Быстрый" + case _: + return "Fast" diff --git a/QtPyHammer/utilities/obj.py b/QtPyHammer/utilities/obj.py index 298a53e..5c8913d 100644 --- a/QtPyHammer/utilities/obj.py +++ b/QtPyHammer/utilities/obj.py @@ -44,7 +44,7 @@ def raycast_intersects(self, ray_direction, ray_length): @staticmethod def load_from_file(filename) -> Obj: # noqa: C901 - """Creates a Obj object from the definition in filename""" + """Creates an Obj object from the definition in filename""" vertex_data = {"v": [], "vt": [], "vn": []} faces = [] current_object = None diff --git a/QtPyHammer/utilities/physics.py b/QtPyHammer/utilities/physics.py index 571a0a2..9bd1cf5 100644 --- a/QtPyHammer/utilities/physics.py +++ b/QtPyHammer/utilities/physics.py @@ -26,6 +26,8 @@ class AxisAlignedBoundingBox: maxs: vector.vec3 def __init__(self, mins: vector.vec3, maxs: vector.vec3): + self.max = None + self.min = None self.mins = vector.vec3(*mins) self.maxs = vector.vec3(*maxs) diff --git a/QtPyHammer/utilities/raycast.py b/QtPyHammer/utilities/raycast.py index 9cbef1a..1fdf988 100644 --- a/QtPyHammer/utilities/raycast.py +++ b/QtPyHammer/utilities/raycast.py @@ -1,86 +1,49 @@ -from typing import Dict, List - -from . import physics -from . import vector - +import math class Ray: - origin: vector.vec3 - direction: vector.vec3 - length: float - def __init__(self, origin, direction, length): self.origin = origin self.direction = direction self.length = length - def plane_intersect(self, plane: physics.Plane): - alignment = vector.dot(plane.normal, self.direction) - if alignment > 0: # skip backfaces - return None # no intersection - # similar method to utilities.solid.clip - origin_distance = vector.dot(plane.normal, self.origin) - plane.distance - end_distance = vector.dot(plane.normal, self.end) - plane.distance - origin_is_behind = bool(origin_distance < 0.01) - end_is_behind = bool(end_distance < 0.01) - if not origin_is_behind and end_is_behind: - t = origin_distance / (origin_distance - end_distance) - return t # lerp(self.origin, self.end, t) for coordinates - # t is great for sorting by intersect depth - - -def raycast_brushes(ray: Ray, brushes: List[object]) -> Dict[int, tuple]: - intersections = dict() - # ^ distance: ("brush", brush.id, brush.face.id) - # -- distance: (type, major_id, minor_id) - # if distance is already in intersection: - # -- z-fighting, special case! (need a margin of error) - for brush in brushes: - - probable_intersections = dict() # just this brush - # ^ {distance(t): face.id} - for face in brush.faces: - plane = physics.Plane(*face.plane) - t = Ray.plane_intersect(plane) - if t is not None: - probable_intersections[t] = face.id - - # check if any probable intersection actually touches the solid - for t, face_id in probable_intersections.items(): - P = vector.lerp(ray.origin, ray.end, t) - valid = True - for face in brush.faces: - if face.id == face_id: - continue # skip yourself, we're checking against all others - other_plane = physics.Plane(*face.plane) - if (vector.dot(other_plane.normal, P) - other_plane.distance) > -0.01: - valid = False # P is floating outside the brush - break - if valid: - brush_t = min(probable_intersections.keys()) - intersections[brush_t] = ("brush", brush.id, face_id) # Renderable.uuid - return intersections - - -def raycast(ray: Ray, map_file): - """Get the object hit by ray""" - intersections = dict() - # ^ distance: ("brush", brush.id, brush.face.id) - # -- distance: (type, major_id, minor_id) - # if distance is already in intersection: - # -- z-fighting, special case! - # TODO: only check against visible / selectable brushes - selectable_brushes = [b for b in map_file.brushes if b.id not in map_file.hidden["brushes"]] - intersections.update(raycast_brushes(ray, selectable_brushes)) - - if len(intersections) == 0: - return None # no intersections, move on - - closest = min(intersections.keys()) # smallest t - # TODO: if other distances are close, give a pop-up list; like Blender alt+select - selected = intersections[closest] - # TODO: selection mode - # -- do we want this object's group?, only the selected face? - # TODO: modifier keys, add to selection? subtract? new selection? - - return selected # let the caller deal with selection mode +def ray_aabb_intersection(ray, aabb_min, aabb_max): + tmin = (aabb_min[0] - ray.origin[0]) / ray.direction[0] + tmax = (aabb_max[0] - ray.origin[0]) / ray.direction[0] + if tmin > tmax: + tmin, tmax = tmax, tmin + + tymin = (aabb_min[1] - ray.origin[1]) / ray.direction[1] + tymax = (aabb_max[1] - ray.origin[1]) / ray.direction[1] + if tymin > tymax: + tymin, tymax = tymax, tymin + + if (tmin > tymax) or (tymin > tmax): + return False + + if tymin > tmin: + tmin = tymin + if tymax < tmax: + tmax = tymax + + tzmin = (aabb_min[2] - ray.origin[2]) / ray.direction[2] + tzmax = (aabb_max[2] - ray.origin[2]) / ray.direction[2] + if tzmin > tzmax: + tzmin, tzmax = tzmax, tzmin + + if (tmin > tzmax) or (tzmin > tmax): + return False + + if tzmin > tmin: + tmin = tzmin + if tzmax < tmax: + tmax = tzmax + + return (tmin < ray.length) and (tmax > 0) + +def raycast(ray, objects): + for obj in objects: + aabb_min = obj.bounding_box_min # Assuming these are lists + aabb_max = obj.bounding_box_max + if ray_aabb_intersection(ray, aabb_min, aabb_max): + return obj # Return the first object hit by the ray + return None diff --git a/README.md b/README.md index 925ae40..f8e3845 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,17 @@ -[![Lines of Code](https://sonarcloud.io/api/project_badges/measure?project=QtPyHammer-devs_QtPyHammer&metric=ncloc)](https://sonarcloud.io/dashboard?id=QtPyHammer-devs_QtPyHammer) -[![Bugs](https://sonarcloud.io/api/project_badges/measure?project=QtPyHammer-devs_QtPyHammer&metric=bugs)](https://sonarcloud.io/dashboard?id=QtPyHammer-devs_QtPyHammer) -[![Reliability Rating](https://sonarcloud.io/api/project_badges/measure?project=QtPyHammer-devs_QtPyHammer&metric=reliability_rating)](https://sonarcloud.io/dashboard?id=QtPyHammer-devs_QtPyHammer) -[![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=QtPyHammer-devs_QtPyHammer&metric=sqale_rating)](https://sonarcloud.io/dashboard?id=QtPyHammer-devs_QtPyHammer) - -# QtPyHammer -A Python alternative to Valve's Hammer Editor (4.X in particular) +# QtPyHammer - Fork +A Python alternative to Valve Hammer Editor 4.x, forked from https://github.com/QtPyHammer-devs/QtPyHammer # Creating the dev environment It's recommended to use a python virtual environment to preserve the version of dependencies, here's how you do that: First, open a terminal in the QtPyHammer-master folder (the top level!) Then, create a new python(3.8+) virtual environment -(Linux users may need to install python-venv first) -`$ python -m venv venv` -Activate your new virtual environment -Windows: `$ call venv/scripts/activate` -Mac / Linux: `$ source venv/bin/activate` +(Linux users may need to install python-venv first `$ python -m venv venv` ) + +## Activate your new virtual environment +* Windows: `$ call venv/scripts/activate` +* Mac / Linux: `$ source venv/bin/activate` + Finally, install all dependencies with pip `$ python -m pip install -r requirements.txt` diff --git a/Team Fortress 2/tf/mapsrc/smeagletest.vmf b/Team Fortress 2/tf/mapsrc/smeagletest.vmf new file mode 100644 index 0000000..4676f6f --- /dev/null +++ b/Team Fortress 2/tf/mapsrc/smeagletest.vmf @@ -0,0 +1,36 @@ +versioninfo +{ + "editorversion" "400" + "editorbuild" "7803" + "mapversion" "0" + "formatversion" "100" + "prefab" "0" +} +viewsettings +{ + "bSnapToGrid" "1" + "bShowGrid" "1" + "bShowLogicalGrid" "0" + "nGridSpacing" "64" + "bShow3DGrid" "0" +} +world +{ + "id" "1" + "mapversion" "0" + "classname" "worldspawn" + "skyname" "sky_tf2_04" + "maxpropscreenwidth" "-1" + "detailvbsp" "detail_2fort.vbsp" + "detailmaterial" "detail/detailsprites_2fort" +} +cameras +{ + "activecamera" "-1" +} +cordon +{ + "mins" "(-1024 -1024 -1024)" + "maxs" "(1024 1024 1024)" + "active" "0" +} diff --git a/configs/games/Team Fortress 2.ini b/configs/games/Team Fortress 2.ini index c7944f7..255b219 100644 --- a/configs/games/Team Fortress 2.ini +++ b/configs/games/Team Fortress 2.ini @@ -1,8 +1,8 @@ [General] -GameDir=Team Fortress 2/tf +GameDir=C:\Program Files (x86)\Steam\steamapps\common\Team Fortress 2\tf [Hammer] -BSP=Team Fortress 2/bin/vbsp.exe +BSP=C:/Program Files (x86)/Steam/steamapps/common/Team Fortress 2/bin/vbsp.exe BSPDir=Team Fortress 2/tf/maps CordonTexture=tools/toolsskybox DefaultLightmapScale=16 @@ -12,9 +12,9 @@ DefaultTextureScale=0.250000 GameData0=Team Fortress 2/bin/tf-puddy.fgd GameExe=Team Fortress 2/hl2.exe GameExeDir=Team Fortress 2 -Light=Team Fortress 2/bin/vrad.exe +Light=C:/Program Files (x86)/Steam/steamapps/common/Team Fortress 2/bin/vrad.exe MapDir=Team Fortress 2/tf/mapsrc MapFormat=4 MaterialExcludeCount=0 TextureFormat=5 -Vis=Team Fortress 2/bin/vvis.exe +Vis=C:/Program Files (x86)/Steam/steamapps/common/Team Fortress 2/bin/vvis.exe diff --git a/configs/preferences.ini b/configs/preferences.ini index b27790b..f682c61 100644 --- a/configs/preferences.ini +++ b/configs/preferences.ini @@ -1,6 +1,7 @@ [General] Game=Team Fortress 2 Theme=light_mode +Language=English [Input] MouseSensitivity=2.0 diff --git a/icons/2DHammerIcon.png b/icons/2DHammerIcon.png new file mode 100644 index 0000000..52d7212 Binary files /dev/null and b/icons/2DHammerIcon.png differ diff --git a/icons/3DHammerIcon.png b/icons/3DHammerIcon.png new file mode 100644 index 0000000..87d7d2d Binary files /dev/null and b/icons/3DHammerIcon.png differ diff --git a/icons/HammerLogo.png b/icons/HammerLogo.png new file mode 100644 index 0000000..76bc64f Binary files /dev/null and b/icons/HammerLogo.png differ diff --git a/icons/LargeGridIcon.png b/icons/LargeGridIcon.png new file mode 100644 index 0000000..ea31ad7 Binary files /dev/null and b/icons/LargeGridIcon.png differ