From bfff4adca214c318be840e6260cbc9cb603aef06 Mon Sep 17 00:00:00 2001 From: Lorenzo Gaifas Date: Thu, 21 Nov 2019 00:38:37 +0100 Subject: [PATCH 1/8] launcher pyqt5 mockup --- openage/launcher/gui.py | 216 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 216 insertions(+) create mode 100644 openage/launcher/gui.py diff --git a/openage/launcher/gui.py b/openage/launcher/gui.py new file mode 100644 index 0000000000..39a8fcb266 --- /dev/null +++ b/openage/launcher/gui.py @@ -0,0 +1,216 @@ +# Copyright 2019-2019 the openage authors. See copying.md for legal info. + +""" +Game Launcher +""" + +import sys +import signal +from PyQt5.QtGui import QFontDatabase, QIcon, QPixmap +from PyQt5.QtWidgets import QApplication, QMainWindow, QHBoxLayout, QVBoxLayout, QWidget, \ + QPushButton, QLabel, QTabWidget, QErrorMessage, QTextBrowser, QTableWidget, \ + QTableWidgetItem, QAbstractItemView +from PyQt5.QtCore import QSize, Qt + + +# assets +logo_path = '../../assets/logo/banner.png' +icon_path = '../../assets/logo/favicon.ico' + + +class ImageLabel(QLabel): + def __init__(self, image_path, max_x, max_y, *args, **kwargs): + super(ImageLabel, self).__init__(*args, **kwargs) + pixmap = QPixmap(image_path).scaled(max_x, max_y, aspectRatioMode=Qt.KeepAspectRatio, + transformMode=Qt.SmoothTransformation) + self.setPixmap(pixmap) + + +class MenuButton(QPushButton): + """ + main menu button class for the openage launcher + """ + def __init__(self, text, icon, background, *args, **kwargs): + super(MenuButton, self).__init__(*args, **kwargs) + self.setFixedSize(QSize(250, 50)) + + # configure font + fixedfont = QFontDatabase.systemFont(QFontDatabase.FixedFont) + fixedfont.setPointSize(15) + self.setFont(fixedfont) + + self.setIcon(QIcon(icon)) + self.setIconSize(QSize(30, 30)) + self.setText(text) + + # background? + + +class NewsBox(QTextBrowser): + """ + rich text browser for openage news with hyperlink + """ + def __init__(self, *args, **kwargs): + super(NewsBox, self).__init__(*args, **kwargs) + self.setOpenExternalLinks(True) + + self.setText('

Check out the Openage Github

' + '

Any HTML should work here.

') + + dummy = ('

Lorem ipsum dolor sit amet, consectetuer adipiscing ' + 'elit. Aenean commodo ligula eget dolor. Aenean massa ' + 'strong. Cum sociis natoque penatibus ' + 'et magnis dis parturient montes, nascetur ridiculus ' + 'mus.

') + + for _ in range(10): + self.append(dummy) + + +class ModTable(QTableWidget): + def __init__(self, *args, **kwargs): + super(ModTable, self).__init__(*args, **kwargs) + self.setColumnCount(4) + self.setHorizontalHeaderLabels(['Activate', 'Name', 'Version', 'Author']) + self.horizontalHeader().setStretchLastSection(True) + self.verticalHeader().hide() + + # disable editability and selectability + self.setSelectionMode(QTableWidget.NoSelection) + self.setFocusPolicy(Qt.NoFocus) + self.setEditTriggers(QAbstractItemView.NoEditTriggers) + + self.mods = [] + + self.add_mod('testmod0', '2.0', 'Someone', False) + self.add_mod('testmod1', '1.0', 'Someone Else', True) + + def add_mod(self, name, version, author, active): + idx = self.rowCount() + self.insertRow(idx) + check = QTableWidgetItem() + check.setCheckState(Qt.Unchecked) + check.setTextAlignment(Qt.AlignCenter) + if active: + check.setCheckState(Qt.Checked) + self.setItem(idx, 0, check) + self.setItem(idx, 1, QTableWidgetItem(name)) + self.setItem(idx, 2, QTableWidgetItem(version)) + self.setItem(idx, 3, QTableWidgetItem(author)) + + +class ModEditButtons(QWidget): + def __init__(self, *args, **kwargs): + super(ModEditButtons, self).__init__(*args, **kwargs) + self.setLayout(QHBoxLayout()) + + self.up = QPushButton('Up') + self.down = QPushButton('Down') + self.add = QPushButton('Add') + self.delete = QPushButton('Delete') + + self.layout().addWidget(self.up) + self.layout().addWidget(self.down) + self.layout().addStretch(1) + self.layout().addWidget(self.add) + self.layout().addWidget(self.delete) + + +class LauncherWindow(QMainWindow): + title = 'Openage Launcher' + + def __init__(self, *args, **kwargs): + super(LauncherWindow, self).__init__(*args, **kwargs) + self.setWindowTitle(LauncherWindow.title) + self.setWindowIcon(QIcon(icon_path)) + self.setMinimumSize(QSize(700, 450)) + + # main widget and main layout + self.main = QWidget() + self.main_layout = QHBoxLayout() + self.main.setLayout(self.main_layout) + self.setCentralWidget(self.main) + + # various sub-layouts and their relative positioning + self.menu_layout = QVBoxLayout() + self.main_layout.addLayout(self.menu_layout) + + self.tabs_layout = QVBoxLayout() + self.main_layout.addLayout(self.tabs_layout) + + self._init_menu() + self._init_tabs() + + self.show() + + def _init_menu(self): + self.logo = ImageLabel(logo_path, 250, 250) + self.menu_layout.addWidget(self.logo) + + self.menu_layout.addStretch(5) + + buttons = [ + ('Continue', self._continue), + ('Play', self._play), + ('Options', self._options), + ('Search for Updates', self._search_for_updates), + ('Quit', self._quit) + ] + + background = None + for button, action in buttons: + button_widget = MenuButton(button, icon_path, background) + button_widget.clicked.connect(action) + self.menu_layout.addWidget(button_widget) + self.menu_layout.addStretch(1) + + self.menu_layout.addStretch(10) + + version = 'dummy version number' + + self.version = QLabel(version) + self.menu_layout.addWidget(self.version) + + def _init_tabs(self): + self.tabs = QTabWidget() + self.tabs_layout.addWidget(self.tabs) + + self.news = NewsBox() + self.tabs.addTab(self.news, 'News') + + self.mods = QWidget() + self.mods.setLayout(QVBoxLayout()) + self.mods.layout().addWidget(ModTable()) + self.mods.layout().addWidget(ModEditButtons()) + + self.tabs.addTab(self.mods, 'Mods') + + def fake_action(self, text): + dialog = QErrorMessage(self) + dialog.showMessage(text) + + def _continue(self): + self.fake_action('Continue!') + + def _play(self): + self.fake_action('Play!') + + def _options(self): + self.fake_action('Options!') + + def _search_for_updates(self): + self.fake_action('Update!') + + def _quit(self): + self.close() + + +if __name__ == '__main__': + app = QApplication(sys.argv) + + launcher_window = LauncherWindow() + + # catch KeyboardInterrupt + signal.signal(signal.SIGINT, signal.SIG_DFL) + + app.exec_() From d30eee8898af11ce69c879c4ad58b216d444a82b Mon Sep 17 00:00:00 2001 From: Lorenzo Gaifas Date: Thu, 21 Nov 2019 13:14:56 +0100 Subject: [PATCH 2/8] better layouts, added content --- openage/launcher/gui.py | 154 +++++++++++++++++++++++++++------------- 1 file changed, 103 insertions(+), 51 deletions(-) diff --git a/openage/launcher/gui.py b/openage/launcher/gui.py index 39a8fcb266..1b7a07fadd 100644 --- a/openage/launcher/gui.py +++ b/openage/launcher/gui.py @@ -19,6 +19,9 @@ class ImageLabel(QLabel): + """ + image widget with antialiased scaling + """ def __init__(self, image_path, max_x, max_y, *args, **kwargs): super(ImageLabel, self).__init__(*args, **kwargs) pixmap = QPixmap(image_path).scaled(max_x, max_y, aspectRatioMode=Qt.KeepAspectRatio, @@ -67,25 +70,29 @@ def __init__(self, *args, **kwargs): self.append(dummy) -class ModTable(QTableWidget): - def __init__(self, *args, **kwargs): - super(ModTable, self).__init__(*args, **kwargs) +class ContentTable(QTableWidget): + # create and add table + def __init__(self, *args, noselect=False, **kwargs): + super(ContentTable, self).__init__(*args, **kwargs) + + # table settings self.setColumnCount(4) self.setHorizontalHeaderLabels(['Activate', 'Name', 'Version', 'Author']) self.horizontalHeader().setStretchLastSection(True) self.verticalHeader().hide() - - # disable editability and selectability - self.setSelectionMode(QTableWidget.NoSelection) + # disable editability, enable row selection + if noselect: + self.setSelectionMode(QAbstractItemView.NoSelection) + else: + self.setSelectionMode(QTableWidget.SingleSelection) + self.setSelectionBehavior(QAbstractItemView.SelectRows) self.setFocusPolicy(Qt.NoFocus) self.setEditTriggers(QAbstractItemView.NoEditTriggers) - self.mods = [] - - self.add_mod('testmod0', '2.0', 'Someone', False) - self.add_mod('testmod1', '1.0', 'Someone Else', True) - - def add_mod(self, name, version, author, active): + def add_entry(self, name, version, author, active): + """ + add an entry to the table and display its activation state + """ idx = self.rowCount() self.insertRow(idx) check = QTableWidgetItem() @@ -99,21 +106,66 @@ def add_mod(self, name, version, author, active): self.setItem(idx, 3, QTableWidgetItem(author)) -class ModEditButtons(QWidget): +class ContentBox(QWidget): def __init__(self, *args, **kwargs): - super(ModEditButtons, self).__init__(*args, **kwargs) - self.setLayout(QHBoxLayout()) + super(ContentBox, self).__init__(*args, **kwargs) + self.setLayout(QVBoxLayout()) - self.up = QPushButton('Up') - self.down = QPushButton('Down') - self.add = QPushButton('Add') - self.delete = QPushButton('Delete') + self.table = ContentTable(noselect=True) + self.layout().addWidget(self.table) - self.layout().addWidget(self.up) - self.layout().addWidget(self.down) - self.layout().addStretch(1) - self.layout().addWidget(self.add) - self.layout().addWidget(self.delete) + # test content + self.table.add_entry('testcontent0', '2.0', 'Someone', False) + self.table.add_entry('testcontent1', '1.0', 'Someone Else', True) + +class ModBox(QWidget): + def __init__(self, *args, **kwargs): + super(ModBox, self).__init__(*args, **kwargs) + self.setLayout(QVBoxLayout()) + + # create and add table + self.table = ContentTable() + self.layout().addWidget(self.table) + + # create and add button layout + self.buttons = QWidget() + self.layout().addWidget(self.buttons) + self.buttons.setLayout(QHBoxLayout()) + + # create and add buttons + self.buttons.up = QPushButton('Up') + self.buttons.down = QPushButton('Down') + self.buttons.add = QPushButton('Add') + self.buttons.delete = QPushButton('Delete') + + self.buttons.layout().addWidget(self.buttons.up) + self.buttons.layout().addWidget(self.buttons.down) + self.buttons.layout().addStretch(1) + self.buttons.layout().addWidget(self.buttons.add) + self.buttons.layout().addWidget(self.buttons.delete) + + # connect buttons to methods + self.buttons.up.pressed.connect(self.mod_up) + self.buttons.down.pressed.connect(self.mod_down) + + # test mods + self.table.add_entry('testmod0', '2.0', 'Someone', False) + self.table.add_entry('testmod1', '1.0', 'Someone Else', True) + + def _move_mod(self, direction): + pass + + def mod_up(self): + """ + increase mod priority + """ + self._move_mod('up') + + def mod_down(self): + """ + decrease mod priority + """ + self._move_mod('down') class LauncherWindow(QMainWindow): @@ -127,16 +179,17 @@ def __init__(self, *args, **kwargs): # main widget and main layout self.main = QWidget() - self.main_layout = QHBoxLayout() - self.main.setLayout(self.main_layout) + self.main.setLayout(QHBoxLayout()) self.setCentralWidget(self.main) # various sub-layouts and their relative positioning - self.menu_layout = QVBoxLayout() - self.main_layout.addLayout(self.menu_layout) + self.left = QWidget() + self.left.setLayout(QVBoxLayout()) + self.main.layout().addWidget(self.left) - self.tabs_layout = QVBoxLayout() - self.main_layout.addLayout(self.tabs_layout) + self.right = QWidget() + self.right.setLayout(QVBoxLayout()) + self.main.layout().addWidget(self.right) self._init_menu() self._init_tabs() @@ -145,63 +198,62 @@ def __init__(self, *args, **kwargs): def _init_menu(self): self.logo = ImageLabel(logo_path, 250, 250) - self.menu_layout.addWidget(self.logo) + self.left.layout().addWidget(self.logo) - self.menu_layout.addStretch(5) + self.left.layout().addStretch(5) buttons = [ - ('Continue', self._continue), - ('Play', self._play), - ('Options', self._options), - ('Search for Updates', self._search_for_updates), - ('Quit', self._quit) + ('Continue', self.continue_), + ('Play', self.play), + ('Options', self.options), + ('Search for Updates', self.search_for_updates), + ('Quit', self.quit) ] background = None for button, action in buttons: button_widget = MenuButton(button, icon_path, background) button_widget.clicked.connect(action) - self.menu_layout.addWidget(button_widget) - self.menu_layout.addStretch(1) + self.left.layout().addWidget(button_widget) + self.left.layout().addStretch(1) - self.menu_layout.addStretch(10) + self.left.layout().addStretch(10) version = 'dummy version number' self.version = QLabel(version) - self.menu_layout.addWidget(self.version) + self.left.layout().addWidget(self.version) def _init_tabs(self): self.tabs = QTabWidget() - self.tabs_layout.addWidget(self.tabs) + self.right.layout().addWidget(self.tabs) self.news = NewsBox() self.tabs.addTab(self.news, 'News') - self.mods = QWidget() - self.mods.setLayout(QVBoxLayout()) - self.mods.layout().addWidget(ModTable()) - self.mods.layout().addWidget(ModEditButtons()) + self.content = ContentBox() + self.tabs.addTab(self.content, 'Content') + self.mods = ModBox() self.tabs.addTab(self.mods, 'Mods') def fake_action(self, text): dialog = QErrorMessage(self) dialog.showMessage(text) - def _continue(self): + def continue_(self): self.fake_action('Continue!') - def _play(self): + def play(self): self.fake_action('Play!') - def _options(self): + def options(self): self.fake_action('Options!') - def _search_for_updates(self): + def search_for_updates(self): self.fake_action('Update!') - def _quit(self): + def quit(self): self.close() From 8ae74bb287fb684698745ba280ff6e2c8cc3f2b9 Mon Sep 17 00:00:00 2001 From: Lorenzo Gaifas Date: Wed, 27 Nov 2019 10:52:47 +0100 Subject: [PATCH 3/8] moved some code, changed add/delete functions --- openage/launcher/gui.py | 67 ++++++++++++++++++++--------------------- 1 file changed, 32 insertions(+), 35 deletions(-) diff --git a/openage/launcher/gui.py b/openage/launcher/gui.py index 1b7a07fadd..84f9a60531 100644 --- a/openage/launcher/gui.py +++ b/openage/launcher/gui.py @@ -49,27 +49,6 @@ def __init__(self, text, icon, background, *args, **kwargs): # background? -class NewsBox(QTextBrowser): - """ - rich text browser for openage news with hyperlink - """ - def __init__(self, *args, **kwargs): - super(NewsBox, self).__init__(*args, **kwargs) - self.setOpenExternalLinks(True) - - self.setText('

Check out the Openage Github

' - '

Any HTML should work here.

') - - dummy = ('

Lorem ipsum dolor sit amet, consectetuer adipiscing ' - 'elit. Aenean commodo ligula eget dolor. Aenean massa ' - 'strong. Cum sociis natoque penatibus ' - 'et magnis dis parturient montes, nascetur ridiculus ' - 'mus.

') - - for _ in range(10): - self.append(dummy) - - class ContentTable(QTableWidget): # create and add table def __init__(self, *args, noselect=False, **kwargs): @@ -106,6 +85,27 @@ def add_entry(self, name, version, author, active): self.setItem(idx, 3, QTableWidgetItem(author)) +class NewsBox(QTextBrowser): + """ + rich text browser for openage news with hyperlink + """ + def __init__(self, *args, **kwargs): + super(NewsBox, self).__init__(*args, **kwargs) + self.setOpenExternalLinks(True) + + self.setText('

Check out the Openage Github

' + '

Any HTML should work here.

') + + dummy = ('

Lorem ipsum dolor sit amet, consectetuer adipiscing ' + 'elit. Aenean commodo ligula eget dolor. Aenean massa ' + 'strong. Cum sociis natoque penatibus ' + 'et magnis dis parturient montes, nascetur ridiculus ' + 'mus.

') + + for _ in range(10): + self.append(dummy) + + class ContentBox(QWidget): def __init__(self, *args, **kwargs): super(ContentBox, self).__init__(*args, **kwargs) @@ -118,6 +118,7 @@ def __init__(self, *args, **kwargs): self.table.add_entry('testcontent0', '2.0', 'Someone', False) self.table.add_entry('testcontent1', '1.0', 'Someone Else', True) + class ModBox(QWidget): def __init__(self, *args, **kwargs): super(ModBox, self).__init__(*args, **kwargs) @@ -145,27 +146,23 @@ def __init__(self, *args, **kwargs): self.buttons.layout().addWidget(self.buttons.delete) # connect buttons to methods - self.buttons.up.pressed.connect(self.mod_up) - self.buttons.down.pressed.connect(self.mod_down) + self.buttons.up.clicked.connect(lambda: self.move_mod(-1)) + self.buttons.down.clicked.connect(lambda: self.move_mod(1)) + self.buttons.add.clicked.connect(self.add_mod) + self.buttons.delete.clicked.connect(self.delete_mod) # test mods self.table.add_entry('testmod0', '2.0', 'Someone', False) self.table.add_entry('testmod1', '1.0', 'Someone Else', True) - def _move_mod(self, direction): - pass + def move_mod(self, step): + print('mod priority changed by {}'.format(step)) - def mod_up(self): - """ - increase mod priority - """ - self._move_mod('up') + def add_mod(self): + print('add a mod') - def mod_down(self): - """ - decrease mod priority - """ - self._move_mod('down') + def delete_mod(self): + print('delete selected mod') class LauncherWindow(QMainWindow): From b2ecfa2197988e64172afd36b5578ce8805dda30 Mon Sep 17 00:00:00 2001 From: Lorenzo Gaifas Date: Wed, 27 Nov 2019 13:28:18 +0100 Subject: [PATCH 4/8] more modular --- openage/launcher/gui.py | 151 +++++++++++++++++++++++----------------- 1 file changed, 87 insertions(+), 64 deletions(-) diff --git a/openage/launcher/gui.py b/openage/launcher/gui.py index 84f9a60531..6fbfa3f916 100644 --- a/openage/launcher/gui.py +++ b/openage/launcher/gui.py @@ -85,30 +85,32 @@ def add_entry(self, name, version, author, active): self.setItem(idx, 3, QTableWidgetItem(author)) -class NewsBox(QTextBrowser): +class NewsTab(QTextBrowser): """ - rich text browser for openage news with hyperlink + rich text browser for openage news with hyperlinks """ def __init__(self, *args, **kwargs): - super(NewsBox, self).__init__(*args, **kwargs) + super(NewsTab, self).__init__(*args, **kwargs) self.setOpenExternalLinks(True) + self.content = self.fetch_news() + self.setText(self.content) - self.setText('

Check out the Openage Github

' - '

Any HTML should work here.

') + def fetch_news(self): + # TODO get news feed + feed = ('

Lorem ipsum dolor sit amet, consectetuer adipiscing ' + 'elit. Aenean commodo ligula eget dolor. Aenean massa ' + 'strong. Cum sociis natoque penatibus ' + 'et magnis dis parturient montes, nascetur ridiculus ' + 'mus.

') * 10 - dummy = ('

Lorem ipsum dolor sit amet, consectetuer adipiscing ' - 'elit. Aenean commodo ligula eget dolor. Aenean massa ' - 'strong. Cum sociis natoque penatibus ' - 'et magnis dis parturient montes, nascetur ridiculus ' - 'mus.

') + # show it + return ('

Check out the Openage Github

' + '

Any HTML should work here.

' + feed) - for _ in range(10): - self.append(dummy) - -class ContentBox(QWidget): +class ContentTab(QWidget): def __init__(self, *args, **kwargs): - super(ContentBox, self).__init__(*args, **kwargs) + super(ContentTab, self).__init__(*args, **kwargs) self.setLayout(QVBoxLayout()) self.table = ContentTable(noselect=True) @@ -119,9 +121,9 @@ def __init__(self, *args, **kwargs): self.table.add_entry('testcontent1', '1.0', 'Someone Else', True) -class ModBox(QWidget): +class ModTab(QWidget): def __init__(self, *args, **kwargs): - super(ModBox, self).__init__(*args, **kwargs) + super(ModTab, self).__init__(*args, **kwargs) self.setLayout(QVBoxLayout()) # create and add table @@ -155,14 +157,71 @@ def __init__(self, *args, **kwargs): self.table.add_entry('testmod0', '2.0', 'Someone', False) self.table.add_entry('testmod1', '1.0', 'Someone Else', True) + def selected_row(self): + if not self.table.selectedItems(): + return None + return self.table.selectedItems()[1].text(), self.table.selectedItems()[1].row() + def move_mod(self, step): - print('mod priority changed by {}'.format(step)) + sel = self.selected_row() + if sel is None: + return + print('mod {}, row {}, changed priority by {}'.format(*sel, step)) def add_mod(self): + # TODO should adding from local and browsing repo happen from the same spot? print('add a mod') def delete_mod(self): - print('delete selected mod') + sel = self.selected_row() + if sel is None: + return + print('delete selected mod: {}, row {}'.format(*sel)) + + +class MainMenu(QWidget): + def __init__(self, *args, **kwargs): + super(MainMenu, self).__init__(*args, **kwargs) + self.setLayout(QVBoxLayout()) + + buttons = [ + ('Continue', self.continue_), + ('Play', self.play_), + ('Editor', self.editor_), + ('Options', self.options_), + ('Search for Updates', self.search_for_updates_), + ('Quit', self.quit_) + ] + + background = None + for button, action in buttons: + button_widget = MenuButton(button, icon_path, background) + button_widget.clicked.connect(action) + self.layout().addWidget(button_widget) + self.layout().addStretch(1) + + def fake_action(self, text): + dialog = QErrorMessage(self) + dialog.showMessage(text) + + def continue_(self): + self.fake_action('Continue!') + + def play_(self): + self.fake_action('Play!') + + def editor_(self): + self.fake_action('Editor!') + + def options_(self): + self.fake_action('Options!') + + def search_for_updates_(self): + self.fake_action('Update!') + + def quit_(self): + # TODO not sure this is a good way to do it + QApplication.quit() class LauncherWindow(QMainWindow): @@ -188,75 +247,39 @@ def __init__(self, *args, **kwargs): self.right.setLayout(QVBoxLayout()) self.main.layout().addWidget(self.right) - self._init_menu() - self._init_tabs() - - self.show() - - def _init_menu(self): + # left side self.logo = ImageLabel(logo_path, 250, 250) self.left.layout().addWidget(self.logo) self.left.layout().addStretch(5) - buttons = [ - ('Continue', self.continue_), - ('Play', self.play), - ('Options', self.options), - ('Search for Updates', self.search_for_updates), - ('Quit', self.quit) - ] - - background = None - for button, action in buttons: - button_widget = MenuButton(button, icon_path, background) - button_widget.clicked.connect(action) - self.left.layout().addWidget(button_widget) - self.left.layout().addStretch(1) - + self.menu = MainMenu() + self.left.layout().addWidget(self.menu) self.left.layout().addStretch(10) version = 'dummy version number' - self.version = QLabel(version) self.left.layout().addWidget(self.version) - def _init_tabs(self): + # right side self.tabs = QTabWidget() self.right.layout().addWidget(self.tabs) - self.news = NewsBox() + self.news = NewsTab() self.tabs.addTab(self.news, 'News') - self.content = ContentBox() + self.content = ContentTab() self.tabs.addTab(self.content, 'Content') - self.mods = ModBox() + self.mods = ModTab() self.tabs.addTab(self.mods, 'Mods') - def fake_action(self, text): - dialog = QErrorMessage(self) - dialog.showMessage(text) - - def continue_(self): - self.fake_action('Continue!') - - def play(self): - self.fake_action('Play!') - - def options(self): - self.fake_action('Options!') - - def search_for_updates(self): - self.fake_action('Update!') - - def quit(self): - self.close() + self.show() if __name__ == '__main__': app = QApplication(sys.argv) - + app.setApplicationName('Openage Launcher') launcher_window = LauncherWindow() # catch KeyboardInterrupt From d99e5018151ef635b7f8977ab40959d12bd32436 Mon Sep 17 00:00:00 2001 From: Lorenzo Gaifas Date: Fri, 24 Jan 2020 13:50:23 +0100 Subject: [PATCH 5/8] minor changes to doc and comments. Added NiceButton class --- openage/launcher/gui.py | 152 ++++++++++++++++++++++++++-------------- 1 file changed, 101 insertions(+), 51 deletions(-) diff --git a/openage/launcher/gui.py b/openage/launcher/gui.py index 6fbfa3f916..267a35fbc7 100644 --- a/openage/launcher/gui.py +++ b/openage/launcher/gui.py @@ -20,41 +20,54 @@ class ImageLabel(QLabel): """ - image widget with antialiased scaling + Image widget with antialiased scaling """ def __init__(self, image_path, max_x, max_y, *args, **kwargs): - super(ImageLabel, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) pixmap = QPixmap(image_path).scaled(max_x, max_y, aspectRatioMode=Qt.KeepAspectRatio, transformMode=Qt.SmoothTransformation) self.setPixmap(pixmap) -class MenuButton(QPushButton): +class NiceButton(QPushButton): """ - main menu button class for the openage launcher + Good-looking button """ - def __init__(self, text, icon, background, *args, **kwargs): - super(MenuButton, self).__init__(*args, **kwargs) - self.setFixedSize(QSize(250, 50)) + def __init__(self, text, *args, icon=None, background=None, text_size=10, **kwargs): + # TODO: implement background + super().__init__(*args, **kwargs) # configure font fixedfont = QFontDatabase.systemFont(QFontDatabase.FixedFont) - fixedfont.setPointSize(15) + fixedfont.setPointSize(text_size) self.setFont(fixedfont) - self.setIcon(QIcon(icon)) - self.setIconSize(QSize(30, 30)) + if icon is not None: + self.setIcon(QIcon(icon)) + self.setIconSize(QSize(30, 30)) + self.setText(text) - # background? + +class MenuButton(NiceButton): + """ + Menu button for the openage launcher + """ + def __init__(self, *args, **kwargs): + super().__init__(*args, text_size=15, **kwargs) + self.setFixedSize(QSize(250, 50)) class ContentTable(QTableWidget): + """ + Displays a table of arbitrary contents and allows + check selection of its members. + """ # create and add table def __init__(self, *args, noselect=False, **kwargs): - super(ContentTable, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) - # table settings + # table settings, hardcoded for now TODO self.setColumnCount(4) self.setHorizontalHeaderLabels(['Activate', 'Name', 'Version', 'Author']) self.horizontalHeader().setStretchLastSection(True) @@ -84,19 +97,28 @@ def add_entry(self, name, version, author, active): self.setItem(idx, 2, QTableWidgetItem(version)) self.setItem(idx, 3, QTableWidgetItem(author)) + def load_content(self, dir_path): + """ + Load all the available content into the table + """ + # TODO + # for thing in dir: + # unpack stuff + # self.add_entry(*stuff) + class NewsTab(QTextBrowser): """ rich text browser for openage news with hyperlinks """ def __init__(self, *args, **kwargs): - super(NewsTab, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.setOpenExternalLinks(True) self.content = self.fetch_news() self.setText(self.content) def fetch_news(self): - # TODO get news feed + # TODO get news feed from somewhere feed = ('

Lorem ipsum dolor sit amet, consectetuer adipiscing ' 'elit. Aenean commodo ligula eget dolor. Aenean massa ' 'strong. Cum sociis natoque penatibus ' @@ -110,7 +132,7 @@ def fetch_news(self): class ContentTab(QWidget): def __init__(self, *args, **kwargs): - super(ContentTab, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.setLayout(QVBoxLayout()) self.table = ContentTable(noselect=True) @@ -123,7 +145,7 @@ def __init__(self, *args, **kwargs): class ModTab(QWidget): def __init__(self, *args, **kwargs): - super(ModTab, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.setLayout(QVBoxLayout()) # create and add table @@ -136,10 +158,10 @@ def __init__(self, *args, **kwargs): self.buttons.setLayout(QHBoxLayout()) # create and add buttons - self.buttons.up = QPushButton('Up') - self.buttons.down = QPushButton('Down') - self.buttons.add = QPushButton('Add') - self.buttons.delete = QPushButton('Delete') + self.buttons.up = NiceButton('Up') + self.buttons.down = NiceButton('Down') + self.buttons.add = NiceButton('Add') + self.buttons.delete = NiceButton('Delete') self.buttons.layout().addWidget(self.buttons.up) self.buttons.layout().addWidget(self.buttons.down) @@ -181,7 +203,7 @@ def delete_mod(self): class MainMenu(QWidget): def __init__(self, *args, **kwargs): - super(MainMenu, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.setLayout(QVBoxLayout()) buttons = [ @@ -195,7 +217,7 @@ def __init__(self, *args, **kwargs): background = None for button, action in buttons: - button_widget = MenuButton(button, icon_path, background) + button_widget = MenuButton(button, icon=icon_path, background=background) button_widget.clicked.connect(action) self.layout().addWidget(button_widget) self.layout().addStretch(1) @@ -228,54 +250,82 @@ class LauncherWindow(QMainWindow): title = 'Openage Launcher' def __init__(self, *args, **kwargs): - super(LauncherWindow, self).__init__(*args, **kwargs) - self.setWindowTitle(LauncherWindow.title) + super().__init__(*args, **kwargs) + + # initialize version and run checks and test + self.version = self.get_version() + if self.is_outdated(): + self.update() + + + + + # construct the ui layouts and populate them + self.build_ui() + + + def build_ui(self): + self.setWindowTitle(self.title) self.setWindowIcon(QIcon(icon_path)) self.setMinimumSize(QSize(700, 450)) # main widget and main layout - self.main = QWidget() - self.main.setLayout(QHBoxLayout()) - self.setCentralWidget(self.main) + self.main_widget = QWidget() + self.main_widget.setLayout(QHBoxLayout()) + self.setCentralWidget(self.main_widget) # various sub-layouts and their relative positioning - self.left = QWidget() - self.left.setLayout(QVBoxLayout()) - self.main.layout().addWidget(self.left) + self.left_widget = QWidget() + self.left_widget.setLayout(QVBoxLayout()) + self.main_widget.layout().addWidget(self.left_widget) - self.right = QWidget() - self.right.setLayout(QVBoxLayout()) - self.main.layout().addWidget(self.right) + self.right_widget = QWidget() + self.right_widget.setLayout(QVBoxLayout()) + self.main_widget.layout().addWidget(self.right_widget) # left side - self.logo = ImageLabel(logo_path, 250, 250) - self.left.layout().addWidget(self.logo) + self.logo_widget = ImageLabel(logo_path, 250, 250) + self.left_widget.layout().addWidget(self.logo_widget) - self.left.layout().addStretch(5) + self.left_widget.layout().addStretch(5) - self.menu = MainMenu() - self.left.layout().addWidget(self.menu) - self.left.layout().addStretch(10) + self.menu_widget = MainMenu() + self.left_widget.layout().addWidget(self.menu_widget) + self.left_widget.layout().addStretch(10) - version = 'dummy version number' - self.version = QLabel(version) - self.left.layout().addWidget(self.version) + self.version_widget = QLabel(self.version) + self.left_widget.layout().addWidget(self.version_widget) # right side - self.tabs = QTabWidget() - self.right.layout().addWidget(self.tabs) + self.tabs_widget = QTabWidget() + self.right_widget.layout().addWidget(self.tabs_widget) - self.news = NewsTab() - self.tabs.addTab(self.news, 'News') + self.news_widget = NewsTab() + self.tabs_widget.addTab(self.news_widget, 'News') - self.content = ContentTab() - self.tabs.addTab(self.content, 'Content') + self.content_widget = ContentTab() + self.tabs_widget.addTab(self.content_widget, 'Content') - self.mods = ModTab() - self.tabs.addTab(self.mods, 'Mods') + self.mods_widget = ModTab() + self.tabs_widget.addTab(self.mods_widget, 'Mods') self.show() + def get_version(self): + return 'dummy version number' + + def is_outdated(self): + latest = 'dummy version number' + if self.version != latest: + return True + return False + + def run_tests(self): + + + def update(self): + print('updating to latest version') + if __name__ == '__main__': app = QApplication(sys.argv) From b46324fd69ba1d19231df2a41987d1a715aedd4a Mon Sep 17 00:00:00 2001 From: Lorenzo Gaifas Date: Sat, 25 Jan 2020 16:04:45 +0100 Subject: [PATCH 6/8] first mockup of pure-python launcher --- openage/launcher/__init__.py | 0 openage/launcher/launcher.py | 109 +++++++++++++++++++++++++++++++++++ openage/launcher/mod.py | 64 ++++++++++++++++++++ 3 files changed, 173 insertions(+) create mode 100644 openage/launcher/__init__.py create mode 100644 openage/launcher/launcher.py create mode 100644 openage/launcher/mod.py diff --git a/openage/launcher/__init__.py b/openage/launcher/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openage/launcher/launcher.py b/openage/launcher/launcher.py new file mode 100644 index 0000000000..5b3b6c9ba6 --- /dev/null +++ b/openage/launcher/launcher.py @@ -0,0 +1,109 @@ +# Copyright 2020-2020 the openage authors. See copying.md for legal info. + +""" +Backend logic of the openage launcher +""" + +from .mod import ModInfo + +LAUNCHER_VERSION = "0.1.0" + + +class LauncherCore: + """ + Holds the launcher status and provides methods for its functionalities + exposes an API for the launcher UI + """ + default_path = 'a/path' + default_repo = 'a.repo.com' + def __init__(self): + self.launcher_version = LAUNCHER_VERSION + self.version = self.get_version() + if self.is_outdated(): + print('hey, update!') + + self.local_mods = set() + self.remote_mods = set() + + self.import_local_mods(self.default_path) + self.import_remote_mods(self.default_repo) + + # store uid of enabled mods, ordered by priority + self.enabled = [] + + @staticmethod + def get_version(self): + """ + returns current openage version + """ + return "0.0.0" + + @staticmethod + def get_latest_version(self): + """ + returns the latest available openage version + """ + return "0.0.0" + + def is_outdated(self): + if self.version < self.get_latest_version(): # TODO: pseudocode + return True + return False + + def import_local_mod(self, file_path): + mod = ModInfo(file_path=file_path) + self.local_mods.add(mod) + + def import_remote_mod(self, url): + mod = ModInfo(url=url) + self.remote_mods.add(mod) + + def import_local_mods(self, mods_dir): + for mod_path in mods_dir: # TODO: pseudocode + self.import_local_mod(mod_path) + + def import_remote_mods(self, repo): + for mod_url in repo: # TODO: pseudocode + self.import_remote_mod(mod_url) + + def download_remote_mod(self, mod_uid): + pass + + def enable_mod(self, mod_uid): + if mod_uid in self.enabled: + return + for other_mod in self.local_mods: + if other_mod in self.enabled and other_mod.conflicts_with(mod_uid): + # complain + return + self.enabled.append(mod_uid) + + def disable_mod(self, mod_uid): + try: + self.enabled.remove(mod_uid) + except ValueError: + # mod is not active + return + + def change_mod_priority(self, mod_uid, shift): + try: + idx = self.enabled.index(mod_uid) + except ValueError: + # mod is not active + return + # this wraps around, feature or bug? + self.enabled[idx], self.enabled[idx + shift] = self.enabled[idx + shift], self.enabled[idx] + + def mod_priority_up(self, mod_uid): + self.change_mod_priority(mod_uid, -1) + + def mod_priority_down(self, mod_uid): + self.change_mod_priority(mod_uid, 1) + + def log_in(self, username): + pass + # ask for password? + + @staticmethod + def get_news_feed(self): + return 'Openage Feed' diff --git a/openage/launcher/mod.py b/openage/launcher/mod.py new file mode 100644 index 0000000000..3388d700c0 --- /dev/null +++ b/openage/launcher/mod.py @@ -0,0 +1,64 @@ +import toml + + +class ModInfo: + """ + Parses and exposes info about a mod + """ + def __init__(self, file_path=None, url=None): + if file_path is not None: + with open(file_path, 'r') as f: + info = toml.load(f) + elif url is not None: + info_str = some_black_magic(url) # TODO: pseudocode + info = toml.loads(info_str) + else: + raise Exception + + self.info = info + + # for convenience + self.uid = self.info['uid'] + self.version = self.info['version'] + self.author = self.info['author'] + self.confl = self.info['conflicts'] + self.req = self.info['requires'] + + def conflicts_with(self, other): + if self in other.confl: + return True + if other in self.confl: + return True + return False + + def requires(self, other): + if other in self.req: + return True + return False + + def required_by(self, other): + return other.requires(self) + + def dump_info(self): + return self.name, self.author, self.version + + def __eq__(self, other): + if isinstance(other, type(self)): + return self.uid == other.uid + # this way we can use the uid to refer to the object + if isinstance(other, int): + return self.uid == other + return NotImplemented + + def __ne__(self, other): + if isinstance(other, type(self)): + return self.uid != other.uid + if isinstance(other, int): + return self.uid != other + return NotImplemented + + def __hash__(self): + return hash(self.uid) + + def __repr__(self): + return f'ModInfo<{self.uid}>' From bcdb8b90982269de32bb53926115d7e91470ec1e Mon Sep 17 00:00:00 2001 From: Lorenzo Gaifas Date: Tue, 4 Feb 2020 12:06:55 +0100 Subject: [PATCH 7/8] simplified modinfo init, lower level api --- openage/launcher/mod.py | 40 ++++++++++++++-------------------------- 1 file changed, 14 insertions(+), 26 deletions(-) diff --git a/openage/launcher/mod.py b/openage/launcher/mod.py index 3388d700c0..d764b152de 100644 --- a/openage/launcher/mod.py +++ b/openage/launcher/mod.py @@ -1,3 +1,9 @@ +# Copyright 2020-2020 the openage authors. See copying.md for legal info. + +""" +Parser for modpack information to be used by the launcher +""" + import toml @@ -5,39 +11,21 @@ class ModInfo: """ Parses and exposes info about a mod """ - def __init__(self, file_path=None, url=None): - if file_path is not None: - with open(file_path, 'r') as f: - info = toml.load(f) - elif url is not None: - info_str = some_black_magic(url) # TODO: pseudocode - info = toml.loads(info_str) - else: - raise Exception - - self.info = info + def __init__(self, info_str): + self.info = toml.loads(info_str) # for convenience self.uid = self.info['uid'] self.version = self.info['version'] self.author = self.info['author'] - self.confl = self.info['conflicts'] - self.req = self.info['requires'] - - def conflicts_with(self, other): - if self in other.confl: - return True - if other in self.confl: - return True - return False + self.conflicts = self.info['conflicts'] + self.requires = self.info['requires'] - def requires(self, other): - if other in self.req: - return True - return False + def get_conflicts(self): + return self.conflicts - def required_by(self, other): - return other.requires(self) + def get_requirements(self): + return self.requires def dump_info(self): return self.name, self.author, self.version From 2843187f8f05a11114bef348fdceefc6f72c2ab1 Mon Sep 17 00:00:00 2001 From: Lorenzo Gaifas Date: Tue, 4 Feb 2020 13:32:32 +0100 Subject: [PATCH 8/8] simpler mod import; streamlined mod activation logic; mod presets --- openage/launcher/launcher.py | 163 ++++++++++++++++++++++++----------- openage/launcher/mod.py | 7 +- 2 files changed, 121 insertions(+), 49 deletions(-) diff --git a/openage/launcher/launcher.py b/openage/launcher/launcher.py index 5b3b6c9ba6..3907d1b406 100644 --- a/openage/launcher/launcher.py +++ b/openage/launcher/launcher.py @@ -4,7 +4,12 @@ Backend logic of the openage launcher """ +from pathlib import Path # should this use openage's Path? +import toml + from .mod import ModInfo +from .. import VERSION +from ..game.main import main as openage_main LAUNCHER_VERSION = "0.1.0" @@ -14,96 +19,158 @@ class LauncherCore: Holds the launcher status and provides methods for its functionalities exposes an API for the launcher UI """ - default_path = 'a/path' - default_repo = 'a.repo.com' + preset_path = Path('another/path') def __init__(self): - self.launcher_version = LAUNCHER_VERSION - self.version = self.get_version() if self.is_outdated(): print('hey, update!') - self.local_mods = set() - self.remote_mods = set() - - self.import_local_mods(self.default_path) - self.import_remote_mods(self.default_repo) + self.mods = {} + # uids of installed mods + self.installed = [] # store uid of enabled mods, ordered by priority self.enabled = [] @staticmethod - def get_version(self): + def get_version(): + """ + returns current launcher version + """ + return LAUNCHER_VERSION + + @staticmethod + def get_latest_version(): """ - returns current openage version + returns the latest launcher version """ return "0.0.0" @staticmethod - def get_latest_version(self): + def get_engine_version(): """ - returns the latest available openage version + returns current launcher version + """ + return VERSION + + @staticmethod + def get_latest_engine_version(): + """ + returns the latest launcher version """ return "0.0.0" def is_outdated(self): - if self.version < self.get_latest_version(): # TODO: pseudocode + if self.get_engine_version() < self.get_latest_engine_version(): # TODO: pseudocode return True return False - def import_local_mod(self, file_path): - mod = ModInfo(file_path=file_path) - self.local_mods.add(mod) + def update(self): + pass - def import_remote_mod(self, url): - mod = ModInfo(url=url) - self.remote_mods.add(mod) + def import_mod_info(self, info_string, path=None): + # retrieval of info_string should be handled by something else + # which also gives a path if the mod is installed locally + mod = ModInfo(info_string, path) + self.mods[mod.uid] = mod + if path is not None: + self.installed.append(mod.uid) - def import_local_mods(self, mods_dir): - for mod_path in mods_dir: # TODO: pseudocode - self.import_local_mod(mod_path) + def install_mod(self, mod_uid, path): + mod = self.mods[mod_uid] + # download and install the mod + mod.set_path(path) + self.installed.append(mod_uid) - def import_remote_mods(self, repo): - for mod_url in repo: # TODO: pseudocode - self.import_remote_mod(mod_url) + @staticmethod + def _mods_conflict(mod1, mod2): + if mod1 in mod2.get_conflicts() or mod2 in mod1.get_conflicts(): + return True + return False - def download_remote_mod(self, mod_uid): - pass + def causes_conflict(self, mod_uid): + # can't check conflicts if mod_info is not available + if not mod_uid in self.mods: + # complain + return + mod = self.mods[mod_uid] + for other_uid in self.enabled: + other = self.mods[other_uid] + if self._mods_conflict(mod, other): + return True + return False + + def get_missing_requirements(self, mod_uid): + # can't check requirements if mod_info is not available + if not mod_uid in self.mods: + # complain + return + mod = self.mods[mod_uid] + missing = [] + for req in mod.get_requirements(): + if req not in self.enabled: + missing.append(req) + return missing def enable_mod(self, mod_uid): if mod_uid in self.enabled: return - for other_mod in self.local_mods: - if other_mod in self.enabled and other_mod.conflicts_with(mod_uid): - # complain - return + if mod_uid not in self.installed: + # complain + return + if self.causes_conflict(mod_uid): + # complain + return + missing_reqs = self.get_missing_requirements(mod_uid) + if missing_reqs: + # complain + return self.enabled.append(mod_uid) def disable_mod(self, mod_uid): - try: - self.enabled.remove(mod_uid) - except ValueError: - # mod is not active + if mod_uid not in self.enabled: return + self.enabled.remove(mod_uid) - def change_mod_priority(self, mod_uid, shift): - try: - idx = self.enabled.index(mod_uid) - except ValueError: - # mod is not active + def set_mod_priority(self, mod_uid, priority): + if mod_uid not in self.enabled: return - # this wraps around, feature or bug? - self.enabled[idx], self.enabled[idx + shift] = self.enabled[idx + shift], self.enabled[idx] + idx = self.enabled.index(mod_uid) + self.enabled.insert(priority, self.enabled.pop(idx)) - def mod_priority_up(self, mod_uid): - self.change_mod_priority(mod_uid, -1) + def get_mods(self): + return list(self.mods.values()) - def mod_priority_down(self, mod_uid): - self.change_mod_priority(mod_uid, 1) + def get_installed_mods(self): + return [mod for mod in self.mods if mod in self.installed] + + def get_enabled_mods(self): + """ + returns ModInfo objects for all the enabled mods + """ + return [mod for mod in self.mods if mod in self.enabled] + + def save_preset(self, preset_name): + preset_path = self.preset_path/preset_name + enabled = {'enabled': self.enabled} + with open(preset_path, 'w+') as f: + toml.dump(enabled, f) + + def load_preset(self, preset_name): + preset_path = self.preset_path/preset_name + if not preset_path.is_file(): + return + with open(preset_path, 'r') as f: + enabled = toml.load(f)['enabled'] + self.enabled = enabled def log_in(self, username): pass # ask for password? @staticmethod - def get_news_feed(self): + def get_news_feed(): return 'Openage Feed' + + @staticmethod + def run(args=None): + openage_main(args, error=None) diff --git a/openage/launcher/mod.py b/openage/launcher/mod.py index d764b152de..8085462f24 100644 --- a/openage/launcher/mod.py +++ b/openage/launcher/mod.py @@ -11,10 +11,12 @@ class ModInfo: """ Parses and exposes info about a mod """ - def __init__(self, info_str): + def __init__(self, info_str, path=None): self.info = toml.loads(info_str) + self.path = path # for convenience + self.name = self.info['name'] self.uid = self.info['uid'] self.version = self.info['version'] self.author = self.info['author'] @@ -30,6 +32,9 @@ def get_requirements(self): def dump_info(self): return self.name, self.author, self.version + def set_path(self, path): + self.path = path + def __eq__(self, other): if isinstance(other, type(self)): return self.uid == other.uid