# Imperialism remake # Copyright (C) 2014-16 Trilarion # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/> """ GUI and internal working of the scenario editor. This is also partly of the client but since the client should not know anything about the scenario, we put it in the server module. """ import math import os from functools import partial from PyQt5 import QtCore, QtGui, QtWidgets from imperialism_remake.client import graphics from imperialism_remake.base import constants, tools from imperialism_remake.lib import qt, utils from imperialism_remake.server.scenario import Scenario class MiniMap(QtWidgets.QWidget): """ Small overview map """ # TODO fixed width -> make it selectable from outside # Fixed width of 300 pixels VIEW_WIDTH = 300 #: signal, emitted if the user clicks somewhere in the mini map and the ROI rectangle changes as a result, sends # the normalized x and y position of the center of the new ROI roi_changed = QtCore.pyqtSignal(float, float) def __init__(self, *args, **kwargs): """ Sets up the graphics view, the toolbar and the tracker rectangle. """ super().__init__(*args, **kwargs) self.setObjectName('mini-map-widget') layout = QtWidgets.QVBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(0) # the content is a scene self.scene = QtWidgets.QGraphicsScene() # tracker rectangle that tracks the view of the map, initially hidden self.tracker = QtWidgets.QGraphicsRectItem() self.tracker.setCursor(QtCore.Qt.PointingHandCursor) self.tracker.setZValue(1000) self.tracker.hide() self.scene.addItem(self.tracker) # the view on the scene (no scroll bars) self.view = QtWidgets.QGraphicsView(self.scene) self.view.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) self.view.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) layout.addWidget(self.view) # the width and height (fixed width throughout the game) # TODO make this adjustable self.view.setFixedWidth(self.VIEW_WIDTH) view_height = math.floor(0.6 * self.VIEW_WIDTH) self.view.setFixedHeight(view_height) # tool bar below the mini map self.toolbar = QtWidgets.QToolBar() self.toolbar.setIconSize(QtCore.QSize(20, 20)) # action group (only one of them can be checked at each time) action_group = QtWidgets.QActionGroup(self.toolbar) # political view in the beginning a = qt.create_action(tools.load_ui_icon('icon.mini.political.png'), 'Show political view', action_group, toggle_connection=self.switch_to_political_view, checkable=True) self.toolbar.addAction(a) # geographical view a = qt.create_action(tools.load_ui_icon('icon.mini.geographical.png'), 'Show geographical view', action_group, toggle_connection=self.switch_to_geographical_view, checkable=True) self.toolbar.addAction(a) self.mode = constants.OverviewMapMode.POLITICAL # wrap tool bar into horizontal layout with stretch l = QtWidgets.QHBoxLayout() l.setContentsMargins(0, 0, 0, 0) l.addWidget(self.toolbar) l.addStretch() # add layout containing tool bar layout.addLayout(l) # graphics items in scene (except the tracker) self.scene_items = [] def redraw(self): """ The scenario has changed or the mode has changed. Redraw the overview map. """ # get number of columns and rows from the scenario columns = editor_scenario.scenario[constants.ScenarioProperty.MAP_COLUMNS] rows = editor_scenario.scenario[constants.ScenarioProperty.MAP_ROWS] # compute tile size (we assume square tiles) tile_size = self.view.width() / columns # adjust view height for aspect ratio of the scenario map, assuming square tiles view_height = math.floor(tile_size * rows) self.view.setFixedHeight(view_height) # remove everything except the tracker from the scene for item in self.scene_items: self.scene.removeItem(item) self.scene_items = [] # set scene rect self.scene.setSceneRect(0, 0, columns * tile_size, rows * tile_size) self.view.fitInView(self.scene.sceneRect()) # by design there should be almost no scaling or anything else if self.mode == constants.OverviewMapMode.POLITICAL: # political mode # fill the ground layer with a neutral color item = self.scene.addRect(0, 0, columns * tile_size, rows * tile_size) item.setBrush(QtCore.Qt.lightGray) item.setPen(qt.TRANSPARENT_PEN) item.setZValue(0) self.scene_items.extend([item]) # draw the nation borders and content (non-smooth) # for all nations for nation in editor_scenario.scenario.nations(): # get nation color color_string = editor_scenario.scenario.nation_property(nation, constants.NationProperty.COLOR) color = QtGui.QColor() color.setNamedColor(color_string) # get all provinces provinces = editor_scenario.scenario.provinces_of_nation(nation) tiles = [] # get all tiles for province in provinces: tiles.extend(editor_scenario.scenario.province_property(province, constants.ProvinceProperty.TILES)) # get rectangular path for each tile path = QtGui.QPainterPath() for tile in tiles: sx, sy = editor_scenario.scenario.scene_position(*tile) path.addRect(sx * tile_size, sy * tile_size, tile_size, tile_size) # simply (creates outline) path = path.simplified() # create a brush from the color brush = QtGui.QBrush(color) item = self.scene.addPath(path, brush=brush) # will use the default pen for outline item.setZValue(1) self.scene_items.extend([item]) elif self.mode == constants.OverviewMapMode.GEOGRAPHICAL: # fill the background with sea (blue) item = self.scene.addRect(0, 0, columns * tile_size, rows * tile_size) item.setBrush(QtCore.Qt.blue) item.setPen(qt.TRANSPARENT_PEN) item.setZValue(0) self.scene_items.extend([item]) # six terrains left, plains, hills, mountains, tundra, swamp, desert # go through each position paths = {} for t in range(1, 7): paths[t] = QtGui.QPainterPath() for column in range(0, columns): for row in range(0, rows): t = editor_scenario.scenario.terrain_at(column, row) if t != 0: # not for sea sx, sy = editor_scenario.scenario.scene_position(column, row) paths[t].addRect(sx * tile_size, sy * tile_size, tile_size, tile_size) colors = {1: QtCore.Qt.green, 2: QtCore.Qt.darkGreen, 3: QtCore.Qt.darkGray, 4: QtCore.Qt.white, 5: QtCore.Qt.darkYellow, 6: QtCore.Qt.yellow} for t in paths: path = paths[t] path = path.simplified() brush = QtGui.QBrush(colors[t]) item = self.scene.addPath(path, brush=brush, pen=qt.TRANSPARENT_PEN) item.setZValue(1) self.scene_items.extend([item]) def switch_to_political_view(self, checked): """ The toolbar button for the political view has been toggled. """ if checked: # mode should not be political self.mode = constants.OverviewMapMode.POLITICAL self.redraw() def switch_to_geographical_view(self, checked): """ The toolbar button for the geographical view has been toggled. """ if checked: # mode should not be geographical self.mode = constants.OverviewMapMode.GEOGRAPHICAL self.redraw() def mousePressEvent(self, event): # noqa: N802 """ The mouse has been pressed inside the view. Center the tracker rectangle. """ super().mouseMoveEvent(event) # if the tracker is not yet visible, don't do anything if not self.tracker.isVisible(): return # get coordinates as scene coordinates and subtract half the tracker width and height tracker_rect = self.tracker.rect() x = event.x() - tracker_rect.width() / 2 y = event.y() - tracker_rect.height() / 2 # apply min/max to keep inside the map area x = min(max(x, 0), self.scene.width() - tracker_rect.width()) y = min(max(y, 0), self.scene.width() - tracker_rect.height()) # check if position of tracker should change if x != tracker_rect.x() or y != tracker_rect.y(): # it should, move tracker and emit signal tracker_rect.moveTo(x, y) self.tracker.setRect(tracker_rect) # normalize position before x = x / self.scene.width() y = y / self.scene.height() self.roi_changed.emit(x, y) def activate_tracker(self, bounds: QtCore.QRectF): """ The main map tells us how large its view is (in terms of the game map) and where it is currently. :param bounds: """ # scale to scene width and height w = self.scene.width() h = self.scene.height() bounds = QtCore.QRectF(bounds.x() * w, bounds.y() * h, bounds.width() * w, bounds.height() * h) # set bounds of tracker and show self.tracker.setRect(bounds) self.tracker.show() class MainMap(QtWidgets.QGraphicsView): """ The big map holding the game map and everything. """ #: signal, emitted if the tile at the mouse pointer (focus) changes focus_changed = QtCore.pyqtSignal(int, int) #: signal, emitted if the change terrain context menu action is called on a terrain change_terrain = QtCore.pyqtSignal(int, int) #: signal, emitted if a province info is requested province_info = QtCore.pyqtSignal(int) #: signal, emitted if a nation info is requested nation_info = QtCore.pyqtSignal(int) def __init__(self): super().__init__() self.setObjectName('map-view') self.scene = QtWidgets.QGraphicsScene() self.setScene(self.scene) self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) self.setTransformationAnchor(QtWidgets.QGraphicsView.NoAnchor) self.setResizeAnchor(QtWidgets.QGraphicsView.NoAnchor) self.setMouseTracking(True) self.current_column = -1 self.current_row = -1 # TODO hardcore tile size somewhere else (and a bit less hard) self.TILE_SIZE = 80 def redraw(self): """ Whenever a scenario is been created or loaded new we need to draw the whole map. """ self.scene.clear() columns = editor_scenario.scenario[constants.ScenarioProperty.MAP_COLUMNS] rows = editor_scenario.scenario[constants.ScenarioProperty.MAP_ROWS] width = (columns + 0.5) * self.TILE_SIZE height = rows * self.TILE_SIZE self.scene.setSceneRect(0, 0, width, height) # TODO should load only once and cache (universal cache), should be soft coded somewhere # load all textures brushes = {0: QtGui.QBrush(QtGui.QColor(64, 64, 255)), 1: QtGui.QBrush(QtGui.QColor(64, 255, 64)), 2: QtGui.QBrush(QtGui.QColor(64, 255, 64)), 3: QtGui.QBrush(QtGui.QColor(64, 255, 64)), 4: QtGui.QBrush(QtGui.QColor(222, 222, 222)), 5: QtGui.QBrush(QtGui.QColor(0, 128, 0)), 6: QtGui.QBrush(QtGui.QColor(222, 222, 0))} # fill the ground layer with ocean item = self.scene.addRect(0, 0, width, height, brush=brushes[0], pen=qt.TRANSPARENT_PEN) item.setZValue(0) # fill plains, hills, mountains, tundra, swamp, desert with texture # go through each position paths = {} for t in range(1, 7): paths[t] = QtGui.QPainterPath() for column in range(0, columns): for row in range(0, rows): t = editor_scenario.scenario.terrain_at(column, row) if t != 0: # not for sea sx, sy = editor_scenario.scenario.scene_position(column, row) paths[t].addRect(sx * self.TILE_SIZE, sy * self.TILE_SIZE, self.TILE_SIZE, self.TILE_SIZE) for t in paths: path = paths[t] path = path.simplified() item = self.scene.addPath(path, brush=brushes[t], pen=qt.TRANSPARENT_PEN) item.setZValue(1) # fill the half tiles which are not part of the map brush = QtGui.QBrush(QtCore.Qt.darkGray) for row in range(0, rows): if row % 2 == 0: item = self.scene.addRect(columns * self.TILE_SIZE, row * self.TILE_SIZE, self.TILE_SIZE / 2, self.TILE_SIZE, pen=qt.TRANSPARENT_PEN) else: item = self.scene.addRect(0, row * self.TILE_SIZE, self.TILE_SIZE / 2, self.TILE_SIZE, pen=qt.TRANSPARENT_PEN) item.setBrush(brush) item.setZValue(1) # draw rivers river_pen = QtGui.QPen(QtGui.QColor(64, 64, 255)) river_pen.setWidth(5) # TODO get rivers via a method (generator) for river in editor_scenario.scenario[constants.ScenarioProperty.RIVERS]: tiles = river['tiles'] path = QtGui.QPainterPath() for tile in tiles: sx, sy = editor_scenario.scenario.scene_position(tile[0], tile[1]) x = (sx + 0.5) * self.TILE_SIZE y = (sy + 0.5) * self.TILE_SIZE if tile == tiles[0]: path.moveTo(x, y) else: path.lineTo(x, y) item = self.scene.addPath(path, pen=river_pen) item.setZValue(2) # draw province and nation borders # TODO the whole border drawing is a crude approximation, implement it the right way province_border_pen = QtGui.QPen(QtGui.QColor(QtCore.Qt.black)) province_border_pen.setWidth(2) nation_border_pen = QtGui.QPen() nation_border_pen.setWidth(4) for nation in editor_scenario.scenario.nations(): # get nation color color = editor_scenario.scenario.nation_property(nation, constants.NationProperty.COLOR) nation_color = QtGui.QColor() nation_color.setNamedColor(color) # get all provinces provinces = editor_scenario.scenario.provinces_of_nation(nation) nation_path = QtGui.QPainterPath() # get all tiles for province in provinces: province_path = QtGui.QPainterPath() tiles = editor_scenario.scenario.province_property(province, constants.ProvinceProperty.TILES) for column, row in tiles: sx, sy = editor_scenario.scenario.scene_position(column, row) province_path.addRect(sx * self.TILE_SIZE, sy * self.TILE_SIZE, self.TILE_SIZE, self.TILE_SIZE) province_path = province_path.simplified() item = self.scene.addPath(province_path, pen=province_border_pen) item.setZValue(4) nation_path.addPath(province_path) nation_path = nation_path.simplified() nation_border_pen.setColor(nation_color) item = self.scene.addPath(nation_path, pen=nation_border_pen) item.setZValue(5) # draw towns and names city_pixmap = QtGui.QPixmap(constants.extend(constants.GRAPHICS_MAP_FOLDER, 'city.png')) for nation in editor_scenario.scenario.nations(): # get all provinces of this nation provinces = editor_scenario.scenario.provinces_of_nation(nation) for province in provinces: column, row = editor_scenario.scenario.province_property(province, constants.ProvinceProperty.TOWN_LOCATION) sx, sy = editor_scenario.scenario.scene_position(column, row) # center city image on center of tile x = (sx + 0.5) * self.TILE_SIZE - city_pixmap.width() / 2 y = (sy + 0.5) * self.TILE_SIZE - city_pixmap.height() / 2 item = self.scene.addPixmap(city_pixmap) item.setOffset(x, y) item.setZValue(6) # display province name below province_name = editor_scenario.scenario.province_property(province, constants.ProvinceProperty.NAME) item = self.scene.addSimpleText(province_name) item.setPen(qt.TRANSPARENT_PEN) item.setBrush(QtGui.QBrush(QtCore.Qt.darkRed)) x = (sx + 0.5) * self.TILE_SIZE - item.boundingRect().width() / 2 y = (sy + 1) * self.TILE_SIZE - item.boundingRect().height() item.setPos(x, y) item.setZValue(6) # display rounded rectangle below province name bx = 8 by = 4 background = QtCore.QRectF(x - bx, y - by, item.boundingRect().width() + 2 * bx, item.boundingRect().height() + 2 * by) path = QtGui.QPainterPath() path.addRoundedRect(background, 50, 50) item = self.scene.addPath(path, pen=qt.TRANSPARENT_PEN, brush=QtGui.QBrush(QtGui.QColor(128, 128, 255, 64))) item.setZValue(5) # draw the grid and the coordinates for column in range(0, columns): for row in range(0, rows): sx, sy = editor_scenario.scenario.scene_position(column, row) # item = self.scene.addRect(sx * self.tile_size, sy * self.tile_size, self.tile_size, self.tile_size) # item.setZValue(1000) text = '({},{})'.format(column, row) item = QtWidgets.QGraphicsSimpleTextItem(text) item.setBrush(QtGui.QBrush(QtCore.Qt.black)) item.setPos((sx + 0.5) * self.TILE_SIZE - item.boundingRect().width() / 2, sy * self.TILE_SIZE) item.setZValue(1001) self.scene.addItem(item) # emit focus changed with -1, -1 self.focus_changed.emit(-1, -1) def visible_rect(self): """ Returns the visible part of the map view relative to the total scene rectangle as a rectangle with normalized values between 0 and 1, relative to the total size of the map. """ # total rectangle of the scene (0, 0, width, height) s = self.scene.sceneRect() # visible rectangle of the view v = self.mapToScene(self.rect()).boundingRect() return QtCore.QRectF(v.x() / s.width(), v.y() / s.height(), v.width() / s.width(), v.height() / s.height()) def set_center_position(self, x, y): """ Changes the visible part of the view by centering the map on normalized positions [0,1) (x,y). """ # total rectangle of the scene (0, 0, width, height) s = self.scene.sceneRect() # visible rectangle of the view v = self.mapToScene(self.rect()).boundingRect() # adjust x, y to scene coordinates and find center x = x * s.width() + v.width() / 2 y = y * s.height() + v.height() / 2 # center on it self.centerOn(x, y) def mouseMoveEvent(self, event): # noqa: N802 """ The mouse on the view has been moved. Emit signal focus_changed if we now hover over a different tile. """ if editor_scenario.scenario is not None: # get mouse position in scene coordinates scene_position = self.mapToScene(event.pos()) / self.TILE_SIZE column, row = editor_scenario.scenario.map_position(scene_position.x(), scene_position.y()) if column != self.current_column or row != self.current_row: self.current_column = column self.current_row = row self.focus_changed.emit(column, row) super().mouseMoveEvent(event) def contextMenuEvent(self, event): # noqa: N802 """ Right click (context click) on a tile. Shows the context menu, depending on the tile position """ # if there is no scenario existing, don't process the context click if not editor_scenario.scenario: return # get mouse position in scene coordinates scene_position = self.mapToScene(event.pos()) / self.TILE_SIZE column, row = editor_scenario.scenario.map_position(scene_position.x(), scene_position.y()) # create context menu menu = QtWidgets.QMenu(self) # change terrain a = qt.create_action(tools.load_ui_icon('icon.editor.change_terrain.png'), 'Set terrain', self, partial(self.change_terrain.emit, column, row)) menu.addAction(a) # is there a province province = editor_scenario.scenario.province_at(column, row) if province: a = qt.create_action(tools.load_ui_icon('icon.editor.province_info.png'), 'Province info', self, partial(self.province_info.emit, province)) menu.addAction(a) # is there also nation nation = editor_scenario.scenario.province_property(province, constants.ProvinceProperty.NATION) if nation: a = qt.create_action(tools.load_ui_icon('icon.editor.nation_info.png'), 'Nation info', self, partial(self.nation_info.emit, nation)) menu.addAction(a) menu.exec(event.globalPos()) class ChangeTerrainWidget(QtWidgets.QGraphicsView): """ """ #: signal, if emitted a new terrain has been chosen terrain_selected = QtCore.pyqtSignal(int) def __init__(self, column, row): super().__init__() self.scene = QtWidgets.QGraphicsScene() self.setScene(self.scene) self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) # TODO see EditorMap redraw brushes = {0: QtGui.QBrush(QtGui.QColor(64, 64, 255)), 1: QtGui.QBrush(QtGui.QColor(64, 255, 64)), 2: QtGui.QBrush(QtGui.QColor(64, 255, 64)), 3: QtGui.QBrush(QtGui.QColor(64, 255, 64)), 4: QtGui.QBrush(QtGui.QColor(222, 222, 222)), 5: QtGui.QBrush(QtGui.QColor(0, 128, 0)), 6: QtGui.QBrush(QtGui.QColor(222, 222, 0))} # TODO hardcore tile size somewhere else (and a bit less hard) self.TILE_SIZE = 80 for i in range(0, 6): y = i // 4 x = i % 4 self.scene.addRect(x * self.TILE_SIZE, y * self.TILE_SIZE, self.TILE_SIZE, self.TILE_SIZE, brush=brushes[i], pen=qt.TRANSPARENT_PEN) class InfoPanel(QtWidgets.QWidget): """ Info box on the right side of the editor. """ def __init__(self): """ Layout. """ super().__init__() self.setObjectName('info-box-widget') layout = QtWidgets.QVBoxLayout(self) self.tile_label = QtWidgets.QLabel() self.tile_label.setTextFormat(QtCore.Qt.RichText) layout.addWidget(self.tile_label) self.province_label = QtWidgets.QLabel() layout.addWidget(self.province_label) self.nation_label = QtWidgets.QLabel() layout.addWidget(self.nation_label) layout.addStretch() def update_tile_info(self, column, row): """ Displays data of a new tile (hovered or clicked in the main map). :param column: The tile column. :param row: The tile row. """ text = 'Position ({}, {})'.format(column, row) terrain = editor_scenario.scenario.terrain_at(column, row) terrain_name = editor_scenario.scenario.terrain_name(terrain) text += '<br>Terrain: {}'.format(terrain_name) province = editor_scenario.scenario.province_at(column, row) if province is not None: name = editor_scenario.scenario.province_property(province, constants.ProvinceProperty.NAME) text += '<br>Province: {}'.format(name) self.tile_label.setText(text) class NewScenarioWidget(QtWidgets.QWidget): """ New scenario dialog. """ #: signal, emitted if this dialog finishes successfully and transmits parameters in the dictionary finished = QtCore.pyqtSignal(object) # see also: https://stackoverflow.com/questions/43964766/pyqt-emit-signal-with-dict # and https://www.riverbankcomputing.com/pipermail/pyqt/2017-May/039175.html # may be changed back to dict with a later PyQt5 version def __init__(self, *args, **kwargs): """ Sets up all the input elements of the create new scenario dialog. """ super().__init__(*args, **kwargs) self.parameters = {} widget_layout = QtWidgets.QVBoxLayout(self) # title box box = QtWidgets.QGroupBox('Title') layout = QtWidgets.QVBoxLayout(box) edit = QtWidgets.QLineEdit() edit.setFixedWidth(300) edit.setPlaceholderText('Unnamed') self.parameters[constants.ScenarioProperty.TITLE] = edit layout.addWidget(edit) widget_layout.addWidget(box) # map size box = QtWidgets.QGroupBox('Map size') layout = QtWidgets.QHBoxLayout(box) layout.addWidget(QtWidgets.QLabel('Width')) edit = QtWidgets.QLineEdit() edit.setFixedWidth(50) edit.setValidator(QtGui.QIntValidator(1, 1000)) edit.setPlaceholderText('100') self.parameters[constants.ScenarioProperty.MAP_COLUMNS] = edit layout.addWidget(edit) layout.addWidget(QtWidgets.QLabel('Height')) edit = QtWidgets.QLineEdit() edit.setFixedWidth(50) edit.setValidator(QtGui.QIntValidator(1, 1000)) edit.setPlaceholderText('60') self.parameters[constants.ScenarioProperty.MAP_ROWS] = edit layout.addWidget(edit) layout.addStretch() widget_layout.addWidget(box) # vertical stretch widget_layout.addStretch() # add confirmation button layout = QtWidgets.QHBoxLayout() toolbar = QtWidgets.QToolBar() a = qt.create_action(tools.load_ui_icon('icon.confirm.png'), 'Create new scenario', toolbar, self.on_ok) toolbar.addAction(a) layout.addStretch() layout.addWidget(toolbar) widget_layout.addLayout(layout) def on_ok(self): """ "Create scenario" has been clicked. """ p = {} # title key = constants.ScenarioProperty.TITLE p[key] = get_text(self.parameters[key]) # number of columns key = constants.ScenarioProperty.MAP_COLUMNS p[key] = int(get_text(self.parameters[key])) # number of rows key = constants.ScenarioProperty.MAP_ROWS p[key] = int(get_text(self.parameters[key])) # TODO conversion can fail, (ValueError) give error message # we close the parent window and emit the appropriate signal self.parent().close() self.finished.emit(p) def get_text(edit: QtWidgets.QLineEdit): """ Returns the text of a line edit. However, if it is empty, it returns the place holder text (whatever there is). :param edit: The line edit :return: The text """ if edit.text(): return edit.text() else: return edit.placeholderText() class ScenarioPropertiesWidget(QtWidgets.QWidget): """ Modify general properties of a scenario dialog. """ # TODO same mechanism like for preferences? def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) widget_layout = QtWidgets.QVBoxLayout(self) # title # TODO validator for title, no empty string self.title_edit = QtWidgets.QLineEdit() self.title_edit.setFixedWidth(300) self.title_edit.setText(editor_scenario.scenario[constants.ScenarioProperty.TITLE]) widget_layout.addLayout(qt.wrap_in_boxlayout((QtWidgets.QLabel('Title'), self.title_edit))) # description self.description_edit = QtWidgets.QLineEdit() self.description_edit.setFixedWidth(300) self.description_edit.setText(editor_scenario.scenario[constants.ScenarioProperty.DESCRIPTION]) widget_layout.addLayout(qt.wrap_in_boxlayout((QtWidgets.QLabel('Description'), self.description_edit))) # game years game_range = editor_scenario.scenario[constants.ScenarioProperty.GAME_YEAR_RANGE] self.game_year_from = QtWidgets.QLineEdit() self.game_year_from.setFixedWidth(100) self.game_year_from.setText(str(game_range[0])) self.game_year_to = QtWidgets.QLineEdit() self.game_year_to.setFixedWidth(100) self.game_year_to.setText(str(game_range[1])) widget_layout.addLayout(qt.wrap_in_boxlayout((QtWidgets.QLabel('Time range from'), self.game_year_from, QtWidgets.QLabel('to'), self.game_year_to))) # vertical stretch widget_layout.addStretch() def on_ok(self): """ We may have changes to apply. """ pass def close_request(self, parent_widget): """ Dialog will be closed, save data. """ editor_scenario.scenario[constants.ScenarioProperty.TITLE] = self.title_edit.text() editor_scenario.scenario[constants.ScenarioProperty.DESCRIPTION] = self.description_edit.text() return True class NationPropertiesWidget(QtWidgets.QWidget): """ Modify nation properties dialog """ # TODO when exiting redraw the big map def __init__(self, initial_nation=None): super().__init__() widget_layout = QtWidgets.QVBoxLayout(self) # toolbar toolbar = QtWidgets.QToolBar() a = qt.create_action(tools.load_ui_icon('icon.add.png'), 'Add nation', toolbar, self.add_nation) toolbar.addAction(a) a = qt.create_action(tools.load_ui_icon('icon.delete.png'), 'Remove nation', toolbar, self.remove_nation) toolbar.addAction(a) widget_layout.addLayout(qt.wrap_in_boxlayout(toolbar)) # nation selection combo box label = QtWidgets.QLabel('Choose') self.nation_combobox = QtWidgets.QComboBox() self.nation_combobox.setFixedWidth(200) self.nation_combobox.currentIndexChanged.connect(self.nation_selected) widget_layout.addWidget(qt.wrap_in_groupbox(qt.wrap_in_boxlayout((label, self.nation_combobox)), 'Nations')) # nation info panel layout = QtWidgets.QVBoxLayout() # description self.description_edit = QtWidgets.QLineEdit() self.description_edit.setFixedWidth(300) self.description_edit.setText('Test') layout.addLayout(qt.wrap_in_boxlayout((QtWidgets.QLabel('Description'), self.description_edit))) # color self.color_picker = QtWidgets.QPushButton() self.color_picker.setFixedSize(24, 24) self.color_picker.clicked.connect(self.show_color_picker) layout.addLayout(qt.wrap_in_boxlayout((QtWidgets.QLabel('Color'), self.color_picker))) # capital province self.capital_province_edit = QtWidgets.QLineEdit() self.capital_province_edit.setFixedWidth(300) layout.addLayout(qt.wrap_in_boxlayout((QtWidgets.QLabel('Capital'), self.capital_province_edit))) # all provinces self.provinces_combobox = QtWidgets.QComboBox() self.provinces_combobox.setFixedWidth(300) self.number_provinces_label = QtWidgets.QLabel() layout.addLayout(qt.wrap_in_boxlayout((self.number_provinces_label, self.provinces_combobox))) widget_layout.addWidget(qt.wrap_in_groupbox(layout, 'Info')) # vertical stretch widget_layout.addStretch() # reset content self.reset_content() # select initial nation if given if initial_nation: index = utils.index_of_element(self.nations, initial_nation) self.nation_combobox.setCurrentIndex(index) def show_color_picker(self): """ Selects a color """ new_color = QtWidgets.QColorDialog.getColor(self.color, parent=self) # isValid() returns True if dialog wasn't cancelled if new_color.isValid(): index = self.nation_combobox.currentIndex() nation = self.nations[index] editor_scenario.scenario.set_nation_property(nation, constants.NationProperty.COLOR, new_color.name()) self.nation_selected(index) def reset_content(self): """ With data. """ # get all nation ids nations = editor_scenario.scenario.nations() # get names for all nations name_of_nation = [(editor_scenario.scenario.nation_property(nation, constants.NationProperty.NAME), nation) for nation in nations] if name_of_nation: name_of_nation = sorted(name_of_nation) # by first element, which is the name nation_names, self.nations = zip(*name_of_nation) else: nation_names = [] self.nations = [] self.nation_combobox.clear() self.nation_combobox.addItems(nation_names) def nation_selected(self, index): """ A nation is selected :param index: """ nation = self.nations[index] self.description_edit.setText(editor_scenario.scenario.nation_property(nation, constants.NationProperty.DESCRIPTION)) province = editor_scenario.scenario.nation_property(nation, constants.NationProperty.CAPITAL_PROVINCE) self.capital_province_edit.setText(editor_scenario.scenario.province_property(province, constants.ProvinceProperty.NAME)) # color color_name = editor_scenario.scenario.nation_property(nation, constants.NationProperty.COLOR) self.color = QtGui.QColor(color_name) self.color_picker.setStyleSheet('QPushButton { background-color: ' + color_name + '; }') provinces = editor_scenario.scenario.nation_property(nation, constants.NationProperty.PROVINCES) provinces_names = [editor_scenario.scenario.province_property(p, constants.ProvinceProperty.NAME) for p in provinces] self.number_provinces_label.setText('Provinces ({})'.format(len(provinces))) self.provinces_combobox.clear() self.provinces_combobox.addItems(provinces_names) def add_nation(self): """ Adds a nation. """ name, ok = QtWidgets.QInputDialog.getText(self, 'Add Nation', 'Name') if ok: # TODO what if nation with the same name already exists # TODO check for sanity of name (no special letters, minimal number of letters) nation = editor_scenario.scenario.add_nation() editor_scenario.scenario.set_nation_property(nation, constants.NationProperty.NAME, name) # reset content self.reset_content() def remove_nation(self): """ Removes a nation. """ index = self.nation_combobox.currentIndex() name = self.nation_combobox.currentText() answer = QtWidgets.QMessageBox.question(self, 'Warning', 'Remove {}'.format(name), QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No, QtWidgets.QMessageBox.Yes) if answer == QtWidgets.QMessageBox.Yes: nation = self.nations[index] # there is no going back on this one editor_scenario.scenario.remove_nation(nation) # reset content self.reset_content() class ProvincePropertiesWidget(QtWidgets.QWidget): """ Modify provinces properties dialog. """ def __init__(self, initial_province=None): super().__init__() widget_layout = QtWidgets.QVBoxLayout(self) # toolbar toolbar = QtWidgets.QToolBar() a = qt.create_action(tools.load_ui_icon('icon.add.png'), 'Add province', toolbar, self.add_province) toolbar.addAction(a) a = qt.create_action(tools.load_ui_icon('icon.delete.png'), 'Remove province', toolbar, self.remove_province) toolbar.addAction(a) widget_layout.addLayout(qt.wrap_in_boxlayout(toolbar)) # provinces selection combo box label = QtWidgets.QLabel('Choose') self.provinces_combobox = QtWidgets.QComboBox() self.provinces_combobox.setFixedWidth(200) self.provinces_combobox.currentIndexChanged.connect(self.province_combobox_index_changed) widget_layout.addWidget(qt.wrap_in_groupbox(qt.wrap_in_boxlayout((label, self.provinces_combobox)), 'provinces')) # province info panel layout = QtWidgets.QVBoxLayout() # nation self.nation_label = QtWidgets.QLabel('Nation') layout.addWidget(self.nation_label) widget_layout.addWidget(qt.wrap_in_groupbox(layout, 'Info')) # vertical stretch widget_layout.addStretch() # reset content self.reset_content() # if province is given, select it if initial_province: index = utils.index_of_element(self.provinces, initial_province) self.provinces_combobox.setCurrentIndex(index) def reset_content(self): """ Resets the content. """ # get all province ids provinces = editor_scenario.scenario.provinces() # get names for all provinces name_of_province = [ (editor_scenario.scenario.province_property(province, constants.ProvinceProperty.NAME), province) for province in provinces] if name_of_province: name_of_province = sorted(name_of_province) # by first element, which is the name province_names, self.provinces = zip(*name_of_province) else: province_names = [] self.provinces = [] self.provinces_combobox.clear() self.provinces_combobox.addItems(province_names) def province_combobox_index_changed(self, index): """ :param index: """ province = self.provinces[index] nation = editor_scenario.scenario.province_property(province, constants.ProvinceProperty.NATION) if nation: self.nation_label.setText(editor_scenario.scenario.nation_property(nation, constants.NationProperty.NAME)) else: self.nation_label.setText('None') def add_province(self): """ Adds a province. """ name, ok = QtWidgets.QInputDialog.getText(self, 'Add Province', 'Name') if ok: # TODO what if province with the same name already exists # TODO check for sanity of name (no special letters, minimal number of letters) province = editor_scenario.scenario.add_province() editor_scenario.scenario.set_province_property(province, constants.ProvinceProperty.NAME, name) # reset content self.reset_content() def remove_province(self): """ Removes a province. """ index = self.provinces_combobox.currentIndex() name = self.provinces_combobox.currentText() answer = QtWidgets.QMessageBox.question(self, 'Warning', 'Remove {}'.format(name), QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No, QtWidgets.QMessageBox.Yes) if answer == QtWidgets.QMessageBox.Yes: province = self.provinces[index] editor_scenario.scenario.remove_province(province) # there is no going back on this one # reset content self.reset_content() class EditorScenario(QtCore.QObject): """ Wrap around the Scenario file to get notified of recreations """ #: signal, scenario has changed completely changed = QtCore.pyqtSignal() def __init__(self): super().__init__() self.scenario = None def load(self, file_name): """ :param file_name: """ if os.path.isfile(file_name): self.scenario = Scenario.from_file(file_name) self.changed.emit() def create(self, properties): """ Create new scenario (from the create new scenario dialog). :param properties: """ self.scenario = Scenario() self.scenario[constants.ScenarioProperty.TITLE] = properties[constants.ScenarioProperty.TITLE] self.scenario.create_empty_map(properties[constants.ScenarioProperty.MAP_COLUMNS], properties[constants.ScenarioProperty.MAP_ROWS]) # standard rules self.scenario[constants.ScenarioProperty.RULES] = 'standard.rules' # self.scenario.load_rules() # TODO rules as extra? rule_file = constants.extend(constants.SCENARIO_RULESET_FOLDER, self.scenario[constants.ScenarioProperty.RULES]) self.scenario._rules = utils.read_as_yaml(rule_file) # emit that everything has changed self.changed.emit() #: static single instance of the editor scenario editor_scenario = EditorScenario() class EditorScreen(QtWidgets.QWidget): """ The screen the contains the whole scenario editor. Is copied into the application main window if the user clicks on the editor pixmap in the client main screen. """ def __init__(self, client): """ Create and setup all the elements. """ super().__init__() # store the client self.client = client # toolbar on top of the window self.toolbar = QtWidgets.QToolBar() self.toolbar.setIconSize(QtCore.QSize(32, 32)) # new, load, save scenario actions a = qt.create_action(tools.load_ui_icon('icon.scenario.new.png'), 'Create new scenario', self, self.new_scenario_dialog) self.toolbar.addAction(a) a = qt.create_action(tools.load_ui_icon('icon.scenario.load.png'), 'Load scenario', self, self.load_scenario_dialog) self.toolbar.addAction(a) a = qt.create_action(tools.load_ui_icon('icon.scenario.save.png'), 'Save scenario', self, self.save_scenario_dialog) self.toolbar.addAction(a) self.toolbar.addSeparator() # edit properties (general, nations, provinces) actions a = qt.create_action(tools.load_ui_icon('icon.editor.general.png'), 'Edit general properties', self, self.general_properties_dialog) self.toolbar.addAction(a) a = qt.create_action(tools.load_ui_icon('icon.editor.nations.png'), 'Edit nations', self, self.nations_dialog) self.toolbar.addAction(a) a = qt.create_action(tools.load_ui_icon('icon.editor.provinces.png'), 'Edit provinces', self, self.provinces_dialog) self.toolbar.addAction(a) # spacer spacer = QtWidgets.QWidget() spacer.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding) self.toolbar.addWidget(spacer) clock = qt.ClockLabel() self.toolbar.addWidget(clock) # help and exit action a = QtWidgets.QAction(tools.load_ui_icon('icon.help.png'), 'Show help', self) a.triggered.connect(client.show_help_browser) # TODO with partial make reference to specific page self.toolbar.addAction(a) a = QtWidgets.QAction(tools.load_ui_icon('icon.back_to_startscreen.png'), 'Exit to main menu', self) a.triggered.connect(client.switch_to_start_screen) # TODO ask if something is changed we should save.. (you might loose progress) self.toolbar.addAction(a) # info box widget self.info_panel = InfoPanel() # main map self.main_map = MainMap() self.main_map.focus_changed.connect(self.info_panel.update_tile_info) self.main_map.change_terrain.connect(self.map_change_terrain) self.main_map.province_info.connect(self.provinces_dialog) self.main_map.nation_info.connect(self.nations_dialog) # mini map self.mini_map = MiniMap() self.mini_map.roi_changed.connect(self.main_map.set_center_position) # connect to editor_scenario editor_scenario.changed.connect(self.scenario_changed) # layout of widgets and toolbar layout = QtWidgets.QGridLayout(self) layout.addWidget(self.toolbar, 0, 0, 1, 2) layout.addWidget(self.mini_map, 1, 0) layout.addWidget(self.info_panel, 2, 0) layout.addWidget(self.main_map, 1, 1, 2, 1) layout.setRowStretch(2, 1) # the info box will take all vertical space left layout.setColumnStretch(1, 1) # the main map will take all horizontal space left def map_change_terrain(self, column, row): """ :param column: :param row: """ content_widget = ChangeTerrainWidget(column, row) dialog = graphics.GameDialog(self.client.main_window, content_widget, title='Change terrain', delete_on_close=True, help_callback=self.client.show_help_browser) #dialog.setFixedSize(QtCore.QSize(900, 700)) dialog.show() def scenario_changed(self): """ Update the GUI in the right order. """ # first repaint the map self.main_map.redraw() # repaint the overview self.mini_map.redraw() # show the tracker rectangle in the overview with the right size self.mini_map.activate_tracker(self.main_map.visible_rect()) def new_scenario_dialog(self): """ Shows the dialog for creation of a new scenario dialog and connect the "create new scenario" signal. """ content_widget = NewScenarioWidget() content_widget.finished.connect(editor_scenario.create) dialog = graphics.GameDialog(self.client.main_window, content_widget, title='New Scenario', delete_on_close=True, help_callback=self.client.show_help_browser) dialog.setFixedSize(QtCore.QSize(600, 400)) dialog.show() def load_scenario_dialog(self): """ Show the load a scenario dialog. Then loads it if the user has selected one. """ # noinspection PyCallByClass file_name = QtWidgets.QFileDialog.getOpenFileName(self, 'Load Scenario', constants.SCENARIO_FOLDER, 'Scenario Files (*.scenario)')[0] if file_name: editor_scenario.load(file_name) self.client.schedule_notification('Loaded scenario {}' .format(editor_scenario.scenario[constants.ScenarioProperty.TITLE])) def save_scenario_dialog(self): """ Show the save a scenario dialog. Then saves it. """ # noinspection PyCallByClass file_name = QtWidgets.QFileDialog.getSaveFileName(self, 'Save Scenario', constants.SCENARIO_FOLDER, 'Scenario Files (*.scenario)')[0] if file_name: editor_scenario.scenario.save(file_name) path, name = os.path.split(file_name) self.client.schedule_notification('Saved to {}'.format(name)) def general_properties_dialog(self): """ Display the modify general properties dialog. """ if not editor_scenario.scenario: return content_widget = ScenarioPropertiesWidget() dialog = graphics.GameDialog(self.client.main_window, content_widget, title='General Properties', delete_on_close=True, help_callback=self.client.show_help_browser, close_callback=content_widget.close_request) # TODO derive meaningful size depending on screen size dialog.setFixedSize(QtCore.QSize(900, 700)) dialog.show() def nations_dialog(self, nation=None): """ Show the modify nations dialog. """ if not editor_scenario.scenario: return content_widget = NationPropertiesWidget(nation) dialog = graphics.GameDialog(self.client.main_window, content_widget, title='Nations', delete_on_close=True, help_callback=self.client.show_help_browser) dialog.setFixedSize(QtCore.QSize(900, 700)) dialog.show() def provinces_dialog(self, province=None): """ Display the modify provinces dialog. """ if not editor_scenario.scenario: return content_widget = ProvincePropertiesWidget(province) dialog = graphics.GameDialog(self.client.main_window, content_widget, title='Provinces', delete_on_close=True, help_callback=self.client.show_help_browser) dialog.setFixedSize(QtCore.QSize(900, 700)) dialog.show()