diff --git a/.gitignore b/.gitignore index 41d9db8..2a44cb2 100644 --- a/.gitignore +++ b/.gitignore @@ -14,4 +14,5 @@ temp/ diagrams/*.svg play*.py tests/__pycache__/*.pyc -untitled.ui +*.ui +*.bat diff --git a/images/3rd_party/sc.png b/images/3rd_party/sc.png new file mode 100644 index 0000000..297626f Binary files /dev/null and b/images/3rd_party/sc.png differ diff --git a/setup.py b/setup.py index d649146..ee1e552 100644 --- a/setup.py +++ b/setup.py @@ -25,29 +25,30 @@ "test", "http", "email", - "distutils" + "distutils", + "ssl" ], "optimize": 2, } setup( name='Joystick Diagrams', - version='1.2.1', + version='1.4', description='Automatically create diagrams for your throttles, joysticks and custom HID devices', long_description=long_description, long_description_content_type='text/markdown', url='https://github.com/Rexeh/joystick-diagrams', author='Robert Cox', - keywords='joystick, HID, diagrams, joystick gremlin', + keywords='joystick, HID, diagrams, joystick gremlin, dcs world', packages=find_packages(), python_requires='>=3.8, <4', install_requires=['pillow'], project_urls={ + 'Documentation' : 'https://joystick-diagrams.com/', 'Bug Reports': 'https://github.com/Rexeh/joystick-diagrams/issues', 'Funding': 'https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=WLLDYGQM5Z39W&source=url', 'Source': 'https://github.com/Rexeh/joystick-diagrams/src', }, - options={'build_exe': build_options}, - executables = [Executable("./src/joystick_diagrams.py", base = base, icon = './images/logo.ico')] + executables = [Executable("./src/joystick_diagrams.py", base = base, icon = './images/logo.ico', copyright='Robert Cox - joystick-diagrams.com')] ) diff --git a/src/adaptors/joystick_gremlin.py b/src/adaptors/joystick_gremlin.py index 5516f0a..19a4cfb 100644 --- a/src/adaptors/joystick_gremlin.py +++ b/src/adaptors/joystick_gremlin.py @@ -25,7 +25,7 @@ def __init__(self,filepath): self.buttonArray = None self.inheritModes = {} self.usingInheritance = False - self. position_map = { + self.position_map = { 1 : 'U', 2 : 'UR', 3 : 'R', diff --git a/src/adaptors/starship_citizen.py b/src/adaptors/starship_citizen.py new file mode 100644 index 0000000..cd417ec --- /dev/null +++ b/src/adaptors/starship_citizen.py @@ -0,0 +1,185 @@ +'''Starship Citizen XML Parser for use with Joystick Diagrams''' +import os +from pathlib import Path +from xml.dom import minidom +import functions.helper as helper +import adaptors.joystick_diagram_interface as jdi + +class StarshipCitizen(jdi.JDinterface): + + def __init__(self, file_path): + jdi.JDinterface.__init__(self) + self.file_path = file_path + self.data = self.__load_file() + self.hat_formats = { + 'up': "U", + 'down': "D", + 'left': "L", + 'right': "R"} + self.hat = None + self.devices = {} + self.button_array = {} + + + def __load_file(self): + if os.path.exists(self.file_path): + if (os.path.splitext(self.file_path))[1] == '.xml': + data = Path(self.file_path).read_text(encoding="utf-8") + try: + self.__validate_file(data) + except Exception: + raise Exception("File is not a valid Starcraft Citizen XML") + else: + return data + else: + raise Exception("File must be an XML file") + else: + raise FileNotFoundError("File not found") + + + def __validate_file(self, data): + try: + parsed_xml = minidom.parseString(data) + except ValueError: + raise Exception("File is not a valid Starcraft Citizen XML") + else: + if (len(parsed_xml.getElementsByTagName('devices'))==1 and + len(parsed_xml.getElementsByTagName('options'))>0 and + len(parsed_xml.getElementsByTagName('actionmap'))>0): + return True + else: + raise Exception + + def parse_map(self, bind_map): + segments = bind_map.split("_") + helper.log("Bind Information: {}".format(segments), 'debug') + bind_device = segments[0] + device_object = self.get_stored_device(bind_device) + helper.log("Device: {}".format(device_object), 'debug') + if device_object is None: + c_map = None + return (device_object,c_map) + if segments[1] == '': + c_map = None + return (device_object,c_map) + elif segments[1][0:6] == 'button': + button_id = segments[1][6:] + c_map = 'BUTTON_{id}'.format(id=button_id) + return (device_object,c_map) + elif segments[1][0:3] == 'hat': + pov_id = segments[1][3:] + pov_dir = self.convert_hat_format(segments[2]) + c_map = 'POV_{id}_{dir}'.format(id=pov_id,dir=pov_dir) + return (device_object,c_map) + elif segments[1][0] in ('x','y','z'): + axis = segments[1][0] + c_map = 'AXIS_{axis}'.format(axis=axis) + return (device_object,c_map) + elif segments[1][0:3] == 'rot': + axis = segments[1][3:] + c_map = 'AXIS_R{axis}'.format(axis=axis) + return (device_object,c_map) + elif segments[1][0:6] == 'slider': + slider_id = segments[1][6:] + c_map = 'AXIS_SLIDER_{id}'.format(id=slider_id ) + return (device_object,c_map) + else: + c_map = None + return (device_object,c_map) + + + def get_human_readable_name(self): + # Future for str replacements + pass + + + def convert_hat_format(self, hat): + #TODO + helper.log("Convert Hat: {}".format(hat), 'debug') + return self.hat_formats[hat] + + + def extract_device_information(self, option): + ''' Accepts parsed OPTION from Starship Citizen XML''' + name = (option.getAttribute('Product')[0:(len(option.getAttribute('Product'))-38)]).strip() + guid = option.getAttribute('Product')[-37:-2] #GUID Fixed + return { + 'name': name, + 'guid' : guid + } + + def get_stored_device(self, device): + + if device in self.devices: + return self.devices[device] + else: + return None + + + def add_device(self, option): + ''' Accepts parsed OPTION from Starship Citizen XML''' + self.devices.update({ + self.device_id(option.getAttribute('type'),option.getAttribute('instance')) : self.extract_device_information(option) + }) + helper.log("Device List: {}".format(self.devices), 'debug') + + + def process_name(self, name): + helper.log("Bind Name: {}".format(name), 'debug') + name = name.split("_") + if len(name) == 1: + return name[0].capitalize() + else: + return (" ".join(name[1:])).capitalize() + + + def build_button_map(self, device,button, name): + if device in self.button_array: + self.button_array[device].update( {button:name } ) + else: + self.button_array.update({ + device : { + button : name + } + }) + + + def device_id(self, device_type, instance): + if device_type == 'keyboard': + t = "kb" + elif device_type == 'joystick': + t = "js" + else: + t = "mo" + return "{type}{instance}".format(type=t, instance=instance) + + + def parse(self): + parse = minidom.parseString(self.data) + joysticks = parse.getElementsByTagName('options') + for j in joysticks: + self.add_device(j) + actions = parse.getElementsByTagName('actionmap') + + for i in actions: + helper.log("Bind Category: {}".format(self.process_name(i.getAttribute('name'))), 'debug') + single_actions = i.getElementsByTagName('action') + for action in single_actions: + name = self.process_name(action.getAttribute('name')) + binds = action.getElementsByTagName('rebind') + helper.log("Binds in group: {}".format(binds), 'debug') + for bind in binds: + bind = bind.getAttribute('input') + button = self.parse_map(bind) + helper.log("Parsed Control: {}".format(button), 'debug') + if(button and button[1] is not None): + helper.log("Valid button, adding to map", 'debug') + self.build_button_map(button[0]['name'], button[1], name) + helper.log("Button Map is now: {}".format(self.button_array)) + else: + helper.log("Button not valid, skipping", 'debug') + + for item in self.button_array: + self.update_joystick_dictionary(item, "Default", False, self.button_array[item]) + + return self.joystick_dictionary diff --git a/src/classes/export.py b/src/classes/export.py index ac9e5a5..f5015e5 100644 --- a/src/classes/export.py +++ b/src/classes/export.py @@ -69,9 +69,7 @@ def create_directory(self,directory): return False def get_template(self, joystick): - joystick = joystick.strip() - if path.exists(self.templates_directory + joystick + ".svg"): data = Path(os.path.join(self.templates_directory, joystick + ".svg")).read_text(encoding="utf-8") return data diff --git a/src/functions/helper.py b/src/functions/helper.py index 4c3448f..9b68a58 100644 --- a/src/functions/helper.py +++ b/src/functions/helper.py @@ -9,19 +9,19 @@ import html # Logging Init -logDir = './logs/' -logFile = 'jv.log' +LOG_DIR = './logs/' +LOG_FILE = 'jv.log' logger = logging.getLogger('jv') webbrowser.register('chrome', None,webbrowser.BackgroundBrowser(config.chrome_path)) -def createDirectory(directory): +def create_directory(directory): if not os.path.exists(directory): return os.makedirs(directory) else: log("Failed to create directory: {}".format(directory), 'error') return False - -def log(text, level='info', exec_s=False): + +def log(text, level='info', exc_info=False): #Accepted Levels # info, warning, error if config.debug: @@ -33,14 +33,14 @@ def log(text, level='info', exec_s=False): logger.error(text, exc_info=True) else: logger.debug(text, exc_info=True) - + def getVersion(): return "Version: " + version.VERSION -if not os.path.exists(logDir): - createDirectory(logDir) -hdlr = logging.FileHandler(logDir + logFile) +if not os.path.exists(LOG_DIR): + create_directory(LOG_DIR) +hdlr = logging.FileHandler(LOG_DIR + LOG_FILE) formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s') hdlr.setFormatter(formatter) logger.addHandler(hdlr) diff --git a/src/joystick_diagrams.py b/src/joystick_diagrams.py index e5a6aa6..3f1dcec 100644 --- a/src/joystick_diagrams.py +++ b/src/joystick_diagrams.py @@ -4,6 +4,7 @@ from ui import Ui_MainWindow import adaptors.dcs_world as dcs import adaptors.joystick_gremlin as jg +import adaptors.starship_citizen as sc import classes.export as export import functions.helper as helper import version @@ -22,6 +23,7 @@ def __init__(self, *args, obj=None, **kwargs): self.dcs_selected_directory_label.setText('') self.dcs_parser_instance = None self.jg_parser_instance = None + self.dcs_easy_mode_checkbox.stateChanged.connect(self.easy_mode_checkbox_action) self.dcs_directory_select_button.clicked.connect(self.set_dcs_directory) self.export_button.clicked.connect(self.export_profiles) @@ -33,6 +35,11 @@ def __init__(self, *args, obj=None, **kwargs): # JG UI Setup self.jg_select_profile_button.clicked.connect(self.set_jg_file) + # SC UI Setup + self.sc_file = None + self.sc_parser_instance = None + self.sc_select_button.clicked.connect(self.set_sc_file) + def setVersion(self): version_text = version.VERSION self.label_9.setText(version_text) @@ -49,6 +56,11 @@ def change_export_button(self): self.export_button.setEnabled(1) else: self.export_button.setDisabled(1) + elif self.parser_selector.currentIndex() == 2: + if self.sc_parser_instance: + self.export_button.setEnabled(1) + else: + self.export_button.setDisabled(1) else: self.export_button.setDisabled(1) @@ -99,6 +111,26 @@ def load_dcs_directory(self): self.dcs_profiles_list.clear() self.dcs_profiles_list.addItems(self.dcs_parser_instance.getValidatedProfiles()) + def set_sc_file(self): + self.clear_info() + self.sc_file = QtWidgets.QFileDialog.getOpenFileName(self,"Select Star Citizen Config file",None,"XMl Files (*.xml)")[0] + if self.sc_file: + self.load_sc_file() + else: + self.print_to_info("No File Selected") + + def load_sc_file(self): + try: + self.sc_parser_instance = sc.StarshipCitizen(self.sc_file) + self.enable_profile_load_button(self.sc_select_button) + self.export_button.setEnabled(1) + self.print_to_info('Succesfully loaded Star Citizen profile') + except Exception as e: + self.disable_profile_load_button(self.sc_select_button) + self.export_button.setEnabled(0) + self.sc_file = None + self.print_to_info("Error Loading File: {}".format(e)) + def set_jg_file(self): self.jg_file = QtWidgets.QFileDialog.getOpenFileName(self,"Select Joystick Gremlin Config file",None,"Gremlin XMl Files (*.xml)")[0] @@ -123,7 +155,7 @@ def load_jg_file(self): self.disable_profile_load_button(self.jg_select_profile_button) self.jg_profile_list.clear() self.export_button.setEnabled(0) - raise + raise Exception(e) def export_profiles(self): if self.parser_selector.currentIndex() == 0: ## JOYSTICK GREMLIN @@ -148,6 +180,9 @@ def export_profiles(self): else: data = self.dcs_parser_instance.processProfiles() self.export_to_svg(data, 'DCS') + elif self.parser_selector.currentIndex() == 2: ## SC + data = self.sc_parser_instance.parse() + self.export_to_svg(data, 'StarCitizen') else: pass # no other tabs have functionality right now diff --git a/src/ui.py b/src/ui.py index 8c1ae52..dd0fdf2 100644 --- a/src/ui.py +++ b/src/ui.py @@ -482,6 +482,25 @@ def setupUi(self, MainWindow): icon2 = QtGui.QIcon() icon2.addPixmap(QtGui.QPixmap("images/3rd_party/dcs.ico"), QtGui.QIcon.Normal, QtGui.QIcon.Off) self.parser_selector.addTab(self.dcs_tab, icon2, "") + self.sc_tab = QtWidgets.QWidget() + self.sc_tab.setObjectName("sc_tab") + self.sc_label = QtWidgets.QLabel(self.sc_tab) + self.sc_label.setGeometry(QtCore.QRect(30, 30, 241, 16)) + self.sc_label.setStyleSheet("color:white") + self.sc_label.setObjectName("sc_label") + self.sc_description_label = QtWidgets.QLabel(self.sc_tab) + self.sc_description_label.setGeometry(QtCore.QRect(30, 50, 261, 20)) + self.sc_description_label.setStyleSheet("font-size: 11px;\n" +"color:white;") + self.sc_description_label.setObjectName("sc_description_label") + self.sc_select_button = QtWidgets.QPushButton(self.sc_tab) + self.sc_select_button.setGeometry(QtCore.QRect(30, 80, 261, 23)) + self.sc_select_button.setStyleSheet("color:white;\n" +"border: 1px solid white;") + self.sc_select_button.setObjectName("sc_select_button") + icon3 = QtGui.QIcon() + icon3.addPixmap(QtGui.QPixmap("images/3rd_party/sc.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off) + self.parser_selector.addTab(self.sc_tab, icon3, "") self.tab = QtWidgets.QWidget() self.tab.setObjectName("tab") self.label_2 = QtWidgets.QLabel(self.tab) @@ -549,9 +568,9 @@ def setupUi(self, MainWindow): "QAbstractButton::text {\n" "color=white;\n" "}") - icon3 = QtGui.QIcon() - icon3.addPixmap(QtGui.QPixmap("images/donate.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off) - self.donate_button.setIcon(icon3) + icon4 = QtGui.QIcon() + icon4.addPixmap(QtGui.QPixmap("images/donate.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off) + self.donate_button.setIcon(icon4) self.donate_button.setIconSize(QtCore.QSize(30, 30)) self.donate_button.setObjectName("donate_button") self.discord_button = QtWidgets.QPushButton(self.centralwidget) @@ -570,9 +589,9 @@ def setupUi(self, MainWindow): "QAbstractButton::text {\n" "color=white;\n" "}") - icon4 = QtGui.QIcon() - icon4.addPixmap(QtGui.QPixmap("images/discord.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off) - self.discord_button.setIcon(icon4) + icon5 = QtGui.QIcon() + icon5.addPixmap(QtGui.QPixmap("images/discord.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off) + self.discord_button.setIcon(icon5) self.discord_button.setIconSize(QtCore.QSize(30, 30)) self.discord_button.setObjectName("discord_button") MainWindow.setCentralWidget(self.centralwidget) @@ -629,6 +648,10 @@ def retranslateUi(self, MainWindow): self.dcs_selected_directory_label.setText(_translate("MainWindow", "c:test")) self.label_10.setText(_translate("MainWindow", "Settings")) self.parser_selector.setTabText(self.parser_selector.indexOf(self.dcs_tab), _translate("MainWindow", "DCS World")) + self.sc_label.setText(_translate("MainWindow", "Star Citizen Config")) + self.sc_description_label.setText(_translate("MainWindow", "Select your .XML config for Star Citizen")) + self.sc_select_button.setText(_translate("MainWindow", "Select your config file")) + self.parser_selector.setTabText(self.parser_selector.indexOf(self.sc_tab), _translate("MainWindow", "Star Citizen")) self.label_2.setText(_translate("MainWindow", "Do you have a game/tool you want to see included?")) self.label_3.setText(_translate("MainWindow", "
Raise an issue on Github - https://github.com/Rexeh/joystick-diagrams
")) self.parser_selector.setTabText(self.parser_selector.indexOf(self.tab), _translate("MainWindow", "+")) diff --git a/src/version.py b/src/version.py index dbef27b..d333f5b 100644 --- a/src/version.py +++ b/src/version.py @@ -1 +1 @@ -VERSION = "1.3" \ No newline at end of file +VERSION = "1.4" \ No newline at end of file diff --git a/tests/data/star_citizen/empty.xml b/tests/data/star_citizen/empty.xml new file mode 100644 index 0000000..e69de29 diff --git a/tests/data/star_citizen/invalid.xml b/tests/data/star_citizen/invalid.xml new file mode 100644 index 0000000..fcfff84 --- /dev/null +++ b/tests/data/star_citizen/invalid.xml @@ -0,0 +1,2 @@ +