def load_game_list(self, game_layout: QGridLayout): while game_layout.count(): child = game_layout.takeAt(0) if child.widget(): child.widget().deleteLater() games = self.all_displays all_entries = iter_entry_points('zero_play.game_display') filtered_entries = self.filter_games(all_entries) for game_entry in filtered_entries: display_class = game_entry.load() display: GameDisplay = display_class() self.destroyed.connect(display.close) # type: ignore display.game_ended.connect(self.on_game_ended) # type: ignore games.append(display) games.sort(key=attrgetter('start_state.game_name')) column_count = math.ceil(math.sqrt(len(games))) for i, display in enumerate(games): row = i // column_count column = i % column_count game_name = display.start_state.game_name game_button = QPushButton(game_name) game_button.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum) game_button.clicked.connect( partial( self.show_game, # type: ignore display)) game_layout.addWidget(game_button, row, column) self.ui.history_game.addItem(game_name, userData=display) if display.rules_path is not None: game_rules_action = self.ui.menu_rules.addAction(game_name) game_rules_action.triggered.connect( partial(self.on_rules, display))
class InfoWidget(QWidget): """Display basic file information in a table (two columns). Parameters ---------- values : dict Each key/value pair in this dict will be displayed in a row, separated by a colon. """ def __init__(self, values=None): super().__init__() vbox = QVBoxLayout(self) self.grid = QGridLayout() self.grid.setColumnStretch(1, 1) vbox.addLayout(self.grid) vbox.addStretch(1) self.set_values(values) def set_values(self, values=None): """Set values (and overwrite existing values). Parameters ---------- values : dict Each key/value pair in this dict is displayed in a row separated by a colon. """ self.clear() if values: for row, (key, value) in enumerate(values.items()): left = QLabel(str(key) + ":") right = QLabel(str(value)) right.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Fixed) self.grid.addWidget(left, row, 0) self.grid.addWidget(right, row, 1) def clear(self): """Clear all values.""" item = self.grid.takeAt(0) while item: item.widget().deleteLater() del item item = self.grid.takeAt(0)
class ResultsGrid(QScrollArea): def __init__(self, parent=None): QScrollArea.__init__(self) self.setParent(parent) # Make QScrollArea transparent self.setStyleSheet( "QScrollArea { background-color: transparent } .QFrame { background-color: transparent }" ) self.setWidgetResizable(True) self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) # Add Touch Gestures to menu. QScroller.grabGesture(self, QScroller.LeftMouseButtonGesture) # Layout settings self.layout = QGridLayout() self.layout.setContentsMargins(0, 0, 0, 0) self.layout.setSpacing(0) # QScrollArea requires a separate QWidget to work properly frame = QFrame() frame.setLayout(self.layout) self.setWidget(frame) def clearResults(self): # Clear the results in the list while self.layout.count(): item = self.layout.takeAt(0) if item.widget() is not None: item.widget().deleteLater() def addResults(self, results): # Add the results to the list # results (list): list of python dict representing results details (playlist or song search) self.clearResults() for i in range(len(results)): item = self.ResultsGridItem(results[i], self) self.layout.addWidget(item, i // 6, i % 6) if results: self.layout.setRowStretch(self.layout.rowCount(), 1) self.layout.setColumnStretch(self.layout.columnCount(), 1) else: # If the results are empty, display no result label = QLabel("没有结果/No Result") label.setStyleSheet("color: white") label.setAlignment(Qt.AlignCenter) font = QFont() font.setPointSize(35) label.setFont(font) self.layout.addWidget(label) class ResultsGridItem(QToolButton): def __init__(self, result, parent=None): QToolButton.__init__(self) self.setParent(parent) self.result = result self.setContentsMargins(0, 0, 0, 0) # Button formatting self.setFixedSize(200, 240) self.setAutoRaise(True) # TODO: change with global themes self.setStyleSheet( "QToolButton:pressed { background-color: rgba(255, 255, 255, 0.1)} QToolButton { background-color: rgba(255, 255, 255, 0.05); border: 1px solid white; color: white}" ) # Set layout settings self.layout = QGridLayout() self.layout.setContentsMargins(0, 0, 0, 0) self.layout.setSpacing(0) if result["type"] == "artists": # Artist image self.formattedImage(self.window().artistPath + result["artist_path"]) self.formattedLabel(result["artist_name"]) # Favourite button self.favouriteButton = QToolButton() self.favouriteButton.setStyleSheet( "QToolButton:pressed { background-color: rgb(31, 41, 75)} QToolButton { background-color: rgb(25, 33, 60);}" ) # Toggle Favourite Icon depending on DB if self.result["favourited"] == 0: self.favouriteButton.isFavourited = False self.favouriteButton.setIcon(QIcon("icons/star.svg")) else: self.favouriteButton.isFavourited = True self.favouriteButton.setIcon( QIcon("icons/star-yellow.svg")) self.favouriteButton.setIconSize(QSize(30, 30)) self.favouriteButton.setFixedSize(70, 70) self.favouriteButton.clicked.connect(self.clickedFavourite) self.layout.addWidget(self.favouriteButton, 0, 0, Qt.AlignRight | Qt.AlignTop) self.clicked.connect(self.clickedArtist) elif result["type"] == "languages": # Language image self.formattedImage(self.window().languagePath + result["language_path"]) self.formattedLabel(result["language_name"]) self.clicked.connect(self.clickedLanguage) self.setLayout(self.layout) def clickedFavourite(self): # Toggle Icon and DB state of song being favourited # For artists only if self.favouriteButton.isFavourited: self.favouriteButton.isFavourited = False self.favouriteButton.setIcon(QIcon("icons/star.svg")) else: self.favouriteButton.isFavourited = True self.favouriteButton.setIcon( QIcon("icons/star-yellow.svg")) DB.setFavouriteArtist(self.result["artist_id"]) def formattedImage(self, path): # Format an image given the path self.setIcon(QIcon(path)) self.setIconSize(QSize(180, 180)) def formattedLabel(self, text): # Format a label given text font = QFont() font.setPixelSize(18) self.setText(text) self.setFont(font) self.setToolButtonStyle(Qt.ToolButtonTextUnderIcon) def clickedArtist(self): # Called when an Artist is clicked on self.window().content.addWidget( WindowSearch( "搜索全部/Search", self, self.window().content.currentWidget().counter + 1, self.result)) self.window().content.setCurrentIndex( self.window().content.currentWidget().counter + 1) def clickedLanguage(self): # Called when a Language is clicked on self.window().content.addWidget( WindowSearch( "搜索歌手/Artist Search", self, self.window().content.currentWidget().counter + 1, self.result, grid=True)) self.window().content.setCurrentIndex( self.window().content.currentWidget().counter + 1)
class MainWindow(QMainWindow): def __init__(self): super(MainWindow, self).__init__() self.ui = Ui_MainWindow() self.ui.setupUi(self) self.art_scene = QGraphicsScene(self) self.art_scene.addText('Open an image file.') self.ui.art_view.setScene(self.art_scene) self.symbols_scene = QGraphicsScene(self) self.ui.symbols_view.setScene(self.symbols_scene) self.ui.action_exit.triggered.connect(self.close) self.ui.action_open_art.triggered.connect(self.open_image) self.ui.action_open_words.triggered.connect(self.open_words) self.ui.action_save.triggered.connect(self.save_pdf) self.ui.action_save_png.triggered.connect(self.save_png) self.ui.action_shuffle.triggered.connect(self.shuffle) self.ui.action_sort.triggered.connect(self.sort) self.ui.rows.valueChanged.connect(self.on_options_changed) self.ui.columns.valueChanged.connect(self.on_options_changed) self.ui.word_clues_radio.toggled.connect(self.on_options_changed) self.ui.symbol_clues_radio.toggled.connect(self.on_options_changed) self.word_layout = QGridLayout(self.ui.word_content) self.ui.word_scroll.setWidgetResizable(True) self.word_labels: typing.Dict[str, QLabel] = {} self.word_shuffler = WordShuffler([]) self.clues = None self.pixmap = self.scaled_pixmap = self.mini_pixmap = None self.sliced_pixmap_item: typing.Optional[QGraphicsPixmapItem] = None self.sliced_image: typing.Optional[QImage] = None self.selection_grid: typing.Optional[SelectionGrid] = None self.cells = [] self.art_shuffler: typing.Optional[ArtShuffler] = None self.symbols_source_pixmap_item: typing.Optional[ QGraphicsPixmapItem] = None self.symbols_pixmap_item: typing.Optional[QGraphicsPixmapItem] = None self.symbols_image: typing.Optional[QImage] = None self.symbols_shuffler: typing.Optional[ArtShuffler] = None self.selected_row: typing.Optional[int] = None self.selected_column: typing.Optional[int] = None self.settings = QSettings() self.image_path: typing.Optional[str] = self.settings.value( 'image_path') self.words_path: typing.Optional[str] = self.settings.value( 'words_path') self.dirty_letters = set() self.timer = QTimer() self.timer.setInterval(500) self.timer.setSingleShot(True) # noinspection PyUnresolvedReferences self.timer.timeout.connect(self.on_dirty) self.row_count = self.column_count = 0 self.clue_type = ClueType.words self.ui.rows.setValue(self.settings.value('row_count', 6, int)) self.ui.columns.setValue(self.settings.value('column_count', 4, int)) clue_type_name = self.settings.value('clue_type', ClueType.words.name) try: clue_type = ClueType[clue_type_name] except KeyError: clue_type = ClueType.words if clue_type == ClueType.words: self.ui.word_clues_radio.setChecked(True) else: self.ui.symbol_clues_radio.setChecked(True) self.row_clues: typing.List[QPixmap] = [] self.column_clues: typing.List[QPixmap] = [] self.on_options_changed() def on_dirty(self): for letter in self.dirty_letters: self.word_labels[letter].setText( self.word_shuffler.make_display(letter)) self.settings.setValue(f'word_{letter}', self.word_shuffler[letter]) if self.dirty_letters: self.clues = self.word_shuffler.make_clues() self.art_shuffler.clues = dict(self.clues) self.dirty_letters.clear() self.on_selection_moved() if self.pixmap is not None: x, y, width, height = self.get_selected_fraction() self.settings.setValue('x', x) self.settings.setValue('y', y) self.settings.setValue('width', width) self.settings.setValue('height', height) new_rows = self.ui.rows.value() new_columns = self.ui.columns.value() if self.ui.word_clues_radio.isChecked(): new_clue_type = ClueType.words else: new_clue_type = ClueType.symbols if (new_rows, new_columns, new_clue_type) == (self.row_count, self.column_count, self.clue_type): return self.settings.setValue('row_count', new_rows) self.settings.setValue('column_count', new_columns) self.settings.setValue('clue_type', new_clue_type.name) self.row_count, self.column_count = new_rows, new_columns self.clue_type = new_clue_type word_count = (self.row_count * self.column_count) while self.word_layout.count(): layout_item = self.word_layout.takeAt(0) layout_item.widget().deleteLater() self.word_labels.clear() self.row_clues.clear() self.column_clues.clear() if self.image_path is not None: self.load_image(self.image_path) if self.words_path is not None: self.load_words(self.words_path) letters = [chr(65 + i) for i in range(word_count)] if self.word_shuffler.needs_blank: letters.insert(0, '') word_fields = {} for i, letter in enumerate(letters): word_field = QLineEdit() self.word_layout.addWidget(word_field, i, 0) # noinspection PyUnresolvedReferences word_field.textEdited.connect(partial(self.on_word_edited, letter)) word_label = QLabel() self.word_layout.addWidget(word_label, i, 1) self.word_labels[letter] = word_label word_fields[letter] = word_field for i, letter in enumerate(letters): word = self.settings.value(f'word_{letter}', '') self.word_shuffler[letter] = word self.dirty_letters.add(letter) word_fields[letter].setText(word) def on_options_changed(self, *_): self.timer.start() def shuffle(self): self.clues = self.word_shuffler.make_clues() if self.art_shuffler is not None: self.art_shuffler.shuffle() self.on_selection_moved() def sort(self): if self.art_shuffler is not None: self.art_shuffler.sort() self.on_selection_moved() def open_words(self): word_filter = 'Text files (*.txt)' if self.words_path is None: words_folder = None else: words_folder = str(Path(self.words_path).parent) file_name, _ = QFileDialog.getOpenFileName(self, "Open a words file.", dir=words_folder, filter=word_filter) if not file_name: return self.settings.setValue('words_path', file_name) self.load_words(file_name) def load_words(self, words_path): with open(words_path) as f: choice = 0 if choice == 0: self.word_shuffler = WordShuffler(f) else: self.word_shuffler = WordStripper(f) def open_image(self): formats = QImageReader.supportedImageFormats() patterns = (f'*.{fmt.data().decode()}' for fmt in formats) image_filter = f'Images ({" ".join(patterns)})' if self.image_path is None: image_folder = None else: image_folder = str(Path(self.image_path).parent) file_name, _ = QFileDialog.getOpenFileName(self, "Open an image file.", dir=image_folder, filter=image_filter) if not file_name: return self.settings.setValue('image_path', file_name) self.load_image(file_name) def load_image(self, image_path): self.pixmap = QPixmap(image_path) if self.pixmap.isNull(): self.pixmap = None self.image_path = image_path self.scale_image() def scale_image(self): if self.pixmap is None: return if self.selection_grid is None: x = self.settings.value('x', 0.0, float) y = self.settings.value('y', 0.0, float) width = self.settings.value('width', 1.0, float) height = self.settings.value('height', 1.0, float) else: x, y, width, height = self.get_selected_fraction() self.art_scene.clear() self.cells.clear() view_size = self.ui.art_view.maximumViewportSize() if view_size.width() == 0: return self.art_scene.setSceneRect(0, 0, view_size.width(), view_size.height()) display_size = QSize(view_size.width() * 0.99 / 2, view_size.height() * 0.99) self.scaled_pixmap = self.pixmap.scaled( display_size, aspectMode=Qt.AspectRatioMode.KeepAspectRatio) self.art_scene.addPixmap(self.scaled_pixmap) scaled_size = self.scaled_pixmap.size() self.selection_grid = SelectionGrid(scaled_size.width() * x, scaled_size.height() * y, scaled_size.width() * width, scaled_size.height() * height, row_count=self.row_count, column_count=self.column_count) self.selection_grid.on_moved = self.on_selection_moved self.art_scene.addItem(self.selection_grid) self.sliced_image = QImage(display_size, QImage.Format.Format_ARGB32_Premultiplied) self.check_clues() self.art_shuffler = ArtShuffler(self.selection_grid.row_count, self.selection_grid.column_count, self.sliced_image, QRect(0, 0, display_size.width(), display_size.height()), clues=self.clues, row_clues=self.row_clues, column_clues=self.column_clues) self.sliced_pixmap_item = self.art_scene.addPixmap( QPixmap.fromImage(self.sliced_image)) self.sliced_pixmap_item.setPos(display_size.width(), 0) self.symbols_scene.clear() self.symbols_source_pixmap_item = self.symbols_scene.addPixmap( self.scaled_pixmap) self.symbols_image = QImage(display_size, QImage.Format.Format_ARGB32_Premultiplied) if self.symbols_shuffler is not None: selected_row = self.symbols_shuffler.selected_row selected_column = self.symbols_shuffler.selected_column else: selected_row = 0 selected_column = None self.symbols_shuffler = ArtShuffler(self.selection_grid.row_count, self.selection_grid.column_count, self.symbols_image, QRect(0, 0, display_size.width(), display_size.height()), row_clues=self.row_clues, column_clues=self.column_clues) self.symbols_shuffler.selected_row = selected_row self.symbols_shuffler.selected_column = selected_column self.symbols_pixmap_item = ClickablePixmapItem( QPixmap.fromImage(self.symbols_image)) self.symbols_pixmap_item.on_click = self.on_symbols_clicked self.symbols_scene.addItem(self.symbols_pixmap_item) self.symbols_pixmap_item.setPos(display_size.width(), 0) self.on_selection_moved() def on_symbols_clicked(self, event: QGraphicsSceneMouseEvent): self.symbols_scene.clearSelection() self.symbols_shuffler.select_clue(event.pos().toPoint()) self.on_selection_moved() def on_word_edited(self, letter, word): self.word_shuffler[letter] = word self.dirty_letters.add(letter) self.timer.start() def get_selected_fraction(self): selection_rect = self.selection_grid.rect() selection_pos = self.selection_grid.pos() size = self.scaled_pixmap.size() x = (selection_pos.x() + selection_rect.x()) / size.width() width = selection_rect.width() / size.width() y = (selection_pos.y() + selection_rect.y()) / size.height() height = selection_rect.height() / size.height() return x, y, width, height def on_selection_moved(self): selected_pixmap = self.get_selected_pixmap() self.art_shuffler.draw(selected_pixmap) self.sliced_pixmap_item.setPixmap(QPixmap.fromImage(self.sliced_image)) selected_pixmap = self.get_selected_pixmap() cell_width = (selected_pixmap.width() / self.selection_grid.column_count) cell_height = (selected_pixmap.height() / self.selection_grid.row_count) self.row_clues.clear() self.column_clues.clear() for i in range(self.selection_grid.row_count): clue_image = selected_pixmap.copy(0, i * cell_height, cell_width, cell_height) self.row_clues.append(clue_image) for j in range(self.selection_grid.column_count): clue_image = selected_pixmap.copy(j * cell_width, 0, cell_width, cell_height) self.column_clues.append(clue_image) self.symbols_shuffler.row_clues = self.row_clues self.symbols_shuffler.column_clues = self.column_clues self.symbols_shuffler.draw_grid(selected_pixmap) self.symbols_pixmap_item.setPixmap( QPixmap.fromImage(self.symbols_image)) self.timer.start() def get_selected_pixmap(self) -> QPixmap: x, y, width, height = self.get_selected_fraction() original_size = self.pixmap.size() selected_pixmap = self.pixmap.copy(x * original_size.width(), y * original_size.height(), width * original_size.width(), height * original_size.height()) return selected_pixmap def resizeEvent(self, event: QResizeEvent): super().resizeEvent(event) self.scale_image() def save_pdf(self): pdf_folder = self.settings.value('pdf_folder') file_name, _ = QFileDialog.getSaveFileName(self, "Save a PDF file.", dir=pdf_folder, filter='Documents (*.pdf)') if not file_name: return self.settings.setValue('pdf_folder', os.path.dirname(file_name)) writer = QPdfWriter(file_name) writer.setPageSize(QPageSize(QPageSize.Letter)) writer.setTitle('Sliced Art Puzzle') writer.setCreator('Don Kirkby') self.paint_puzzle(writer) def save_png(self): pdf_folder = self.settings.value('pdf_folder') file_name, _ = QFileDialog.getSaveFileName(self, "Save an image file.", dir=pdf_folder, filter='Images (*.png)') if not file_name: return writer = QPixmap(1000, 2000) self.paint_puzzle(writer) writer.save(file_name) self.settings.setValue('pdf_folder', os.path.dirname(file_name)) def paint_puzzle(self, writer: QPaintDevice): self.check_clues() painter = QPainter(writer) try: print_shuffler = ArtShuffler(self.art_shuffler.rows, self.art_shuffler.cols, writer, QRect(0, 0, writer.width(), round(writer.height() / 2)), clues=self.clues, row_clues=self.row_clues, column_clues=self.column_clues) print_shuffler.cells = self.art_shuffler.cells[:] print_shuffler.is_shuffled = self.art_shuffler.is_shuffled selected_pixmap = self.get_selected_pixmap() print_shuffler.draw(selected_pixmap, painter) print_shuffler.rect.moveTop(writer.height() / 2) print_shuffler.draw_grid(selected_pixmap, painter) finally: painter.end() def check_clues(self): if self.clue_type == ClueType.words: if self.clues is None: self.clues = self.word_shuffler.make_clues() self.row_clues.clear() self.column_clues.clear() else: self.clues = None
class SettingWidget(QWidget): settings_changed_signal = QtCore.Signal() def __init__(self, parent, settings=None, **kwargs): super().__init__(parent, **kwargs) self.setObjectName("settings_widget") self.group_box = QGroupBox("Настройки") self.layout = QGridLayout() self.layout.setObjectName('settings_layout') self.layout.setColumnStretch(0, 6) self.layout.setColumnStretch(1, 1) self.group_box.setLayout(self.layout) self.widgets = [] self.change_settings(settings) def change_settings(self, settings): # First clean up all previous items for i in range(self.layout.count()): self.layout.removeItem(self.layout.takeAt(0)) for w in self.widgets: w.deleteLater() self.widgets = [] if not settings: label = QLabel(self.parentWidget()) label.setObjectName("settings_empty_label") label.setText("Для данной операции настройки отсутствуют") label.setWordWrap(True) self.widgets.append(label) self.layout.addWidget(label, 0, 0) else: x = 0 for s in settings: if isinstance(s.type, range): label = QLabel(self.parentWidget()) label.setText(s.text) label.setWordWrap(True) self.widgets.append(label) self.layout.addWidget(label, x, 0) slider = QSlider(self.parentWidget()) slider.setOrientation(Qt.Horizontal) slider.setMinimum(s.type.start // s.type.step) slider.setMaximum(s.type.stop // s.type.step - 1) slider.setValue(s.value // s.type.step) self.layout.addWidget(slider, x + 1, 0) line_edit = QLineEdit(self.parentWidget()) line_edit.setText(str(s.value)) self.layout.addWidget(line_edit, x + 1, 1) slider.valueChanged.connect( partial(self.slider_change, line_edit, s)) line_edit.returnPressed.connect( partial(self.line_edit_change, slider, line_edit, s)) self.widgets.append(slider) self.widgets.append(line_edit) x += 2 elif s.type == bool: checkbox = QCheckBox(self.parentWidget()) checkbox.setText('\n'.join(wrap(s.text, 40))) checkbox.setChecked(s.value) checkbox.clicked.connect( partial(self.checkbox_changed, checkbox, s)) self.layout.addWidget(checkbox, x, 0) self.widgets.append(checkbox) x += 1 elif s.type in (int, float, str): pass def slider_change(self, line_edit: QLineEdit, settings_item, value): value *= settings_item.type.step if settings_item.value != value: line_edit.setText(str(value)) settings_item.value = value self.settings_changed_signal.emit() def line_edit_change(self, slider: QSlider, line_edit: QLineEdit, settings_item): value = line_edit.text() try: if settings_item.type == int or isinstance(settings_item.type, range): value = int(value) elif settings_item.type == float: value = float(value) if isinstance(settings_item.type, range) and value not in settings_item.type: raise ValueError except ValueError: line_edit.setText(str(settings_item.value)) return if settings_item.value != value: settings_item.value = value if slider is not None: slider.setValue(value // settings_item.type.step) self.settings_changed_signal.emit() def checkbox_changed(self, checkbox: QCheckBox, settings_item): if settings_item.value != checkbox.isChecked(): settings_item.value = checkbox.isChecked() self.settings_changed_signal.emit()