Skip to content

Commit

Permalink
Add 'select' buttons to obervation and taxon pages instead of selecti…
Browse files Browse the repository at this point in the history
…ng automatically by browsing (#392)

Closes #257
  • Loading branch information
JWCook authored Jul 2, 2024
2 parents acce458 + 47654a2 commit c566795
Show file tree
Hide file tree
Showing 11 changed files with 239 additions and 136 deletions.
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2022 Jordan Cook
Copyright (c) 2024 Jordan Cook

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

Expand Down
2 changes: 1 addition & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

# Basic config
project = 'Naturtag'
copyright = '2022, Jordan Cook'
copyright = '2024, Jordan Cook'
author = 'Jordan Cook'
html_static_path = ['_static']
templates_path = ['_templates']
Expand Down
79 changes: 51 additions & 28 deletions naturtag/app/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,28 +86,6 @@ def __init__(self, app: NaturtagApp):
self.taxon_controller.on_message.connect(self.info)
self.observation_controller.on_message.connect(self.info)

# Select observation/taxon from image context menu, ID input fields, and iconic taxa filters
self.image_controller.gallery.on_select_taxon.connect(self.taxon_controller.select_taxon)
self.image_controller.gallery.on_select_observation.connect(
self.observation_controller.select_observation
)
self.image_controller.on_select_observation_id.connect(
self.observation_controller.select_observation
)
self.image_controller.on_select_taxon_id.connect(self.taxon_controller.select_taxon)
self.taxon_controller.search.iconic_taxon_filters.on_select.connect(
self.taxon_controller.select_taxon
)

# Update photo tab when a taxon is selected
self.taxon_controller.on_select.connect(self.image_controller.select_taxon)

# Update photo and taxon tabs when an observation is selected
self.observation_controller.on_select.connect(self.image_controller.select_observation)
self.observation_controller.on_select.connect(
lambda obs: self.taxon_controller.display_taxon(obs.taxon, notify=False)
)

# Settings that take effect immediately
self.settings_menu.all_ranks.on_click.connect(self.taxon_controller.search.reset_ranks)
self.settings_menu.dark_mode.on_click.connect(set_theme)
Expand All @@ -133,12 +111,48 @@ def __init__(self, app: NaturtagApp):
)
self.tabs.setTabVisible(self.log_tab_idx, self.app.settings.show_logs)

# Switch to different tab if requested from Photos tab
self.image_controller.on_select_taxon_tab.connect(
lambda: self.tabs.setCurrentWidget(self.taxon_controller)
# Photos tab: view taxon and switch tab
self.image_controller.gallery.on_view_taxon_id.connect(
self.taxon_controller.display_taxon_by_id
)
self.image_controller.gallery.on_view_taxon_id.connect(self.switch_tab_taxa)
self.image_controller.on_view_taxon_id.connect(self.taxon_controller.display_taxon_by_id)
self.image_controller.on_view_taxon_id.connect(self.switch_tab_taxa)

# Photos tab: view observation and switch tab
self.image_controller.gallery.on_view_observation_id.connect(
self.observation_controller.display_observation_by_id
)
self.image_controller.on_select_observation_tab.connect(
lambda: self.tabs.setCurrentWidget(self.observation_controller)
self.image_controller.gallery.on_view_observation_id.connect(self.switch_tab_observations)
self.image_controller.on_view_observation_id.connect(
self.observation_controller.display_observation_by_id
)
self.image_controller.on_view_observation_id.connect(self.switch_tab_observations)

# Species tab: View observation and switch tab
# self.taxon_controller.taxon_info.on_view_observations.connect(
# lambda obs: self.observation_controller.display_observation(obs, notify=False)
# )
# self.taxon_controller.taxon_info.on_view_observations.connect(self.switch_tab_observations)

# Species tab: Select taxon for tagging and switch to Photos tab
self.taxon_controller.taxon_info.on_select.connect(self.image_controller.select_taxon)
self.taxon_controller.taxon_info.on_select.connect(
lambda: self.tabs.setCurrentWidget(self.image_controller)
)

# Observations tab: View taxon and switch tab
self.observation_controller.obs_info.on_view_taxon.connect(
lambda taxon: self.taxon_controller.display_taxon(taxon, notify=False)
)
self.observation_controller.obs_info.on_view_taxon.connect(self.switch_tab_taxa)

# Observations tab: Select observation for tagging and switch to Photos tab
self.observation_controller.obs_info.on_select.connect(
self.image_controller.select_observation
)
self.observation_controller.obs_info.on_select.connect(
lambda: self.tabs.setCurrentWidget(self.image_controller)
)

# Connect file picker <--> recent/favorite dirs
Expand Down Expand Up @@ -176,7 +190,7 @@ def __init__(self, app: NaturtagApp):
QShortcut(QKeySequence('F9'), self).activated.connect(self.reload_qss)
demo_images = list((ASSETS_DIR / 'demo_images').glob('*.jpg'))
self.image_controller.gallery.load_images(demo_images) # type: ignore
self.observation_controller.select_observation(56830941)
self.observation_controller.display_observation_by_id(56830941)

def check_username(self):
"""If username isn't saved, show popup dialog to prompt user to enter it"""
Expand Down Expand Up @@ -255,6 +269,15 @@ def show_settings(self):
"""Show the settings menu"""
self.settings_menu.show()

def switch_tab_observations(self):
self.tabs.setCurrentWidget(self.observation_controller)

def switch_tab_taxa(self):
self.tabs.setCurrentWidget(self.taxon_controller)

def switch_tab_photos(self):
self.tabs.setCurrentWidget(self.image_controller)

def toggle_fullscreen(self) -> bool:
"""Toggle fullscreen, and change icon for toolbar fullscreen button"""
if not self.isFullScreen():
Expand Down
89 changes: 58 additions & 31 deletions naturtag/controllers/image_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
from typing import Optional

from pyinaturalist import Observation, Taxon
from PySide6.QtCore import Qt, Signal, Slot
from PySide6.QtCore import Qt, QThread, Signal, Slot
from PySide6.QtWidgets import QApplication, QGroupBox, QLabel, QSizePolicy

from naturtag.controllers import BaseController, ImageGallery
from naturtag.controllers import BaseController, ImageGallery, get_app
from naturtag.metadata import MetaMetadata, _refresh_tags, tag_images
from naturtag.utils import get_ids_from_url
from naturtag.widgets import (
Expand All @@ -24,10 +24,8 @@ class ImageController(BaseController):
"""Controller for selecting and tagging local image files"""

on_new_metadata = Signal(MetaMetadata) #: Metadata for an image was updated
on_select_taxon_id = Signal(int) #: A taxon ID was entered
on_select_taxon_tab = Signal() #: Request to switch to taxon tab
on_select_observation_id = Signal(int) #: An observation ID was entered
on_select_observation_tab = Signal() #: Request to switch to observation tab
on_view_taxon_id = Signal(int) #: Request to switch to taxon tab
on_view_observation_id = Signal(int) #: Request to switch to observation tab

def __init__(self):
super().__init__()
Expand All @@ -45,16 +43,14 @@ def __init__(self):

# Input fields
inputs_layout = VerticalLayout(group_box)
self.input_obs_id = IdInput()
inputs_layout.addWidget(QLabel('Observation ID:'))
inputs_layout.addWidget(self.input_obs_id)
self.input_taxon_id = IdInput()
self.input_taxon_id.on_select.connect(self.select_taxon_by_id)
inputs_layout.addWidget(QLabel('Taxon ID:'))
inputs_layout.addWidget(self.input_taxon_id)

# Notify other controllers when an ID is selected from input text
self.input_obs_id.on_select.connect(self.on_select_observation_id)
self.input_taxon_id.on_select.connect(self.on_select_taxon_id)
self.input_obs_id = IdInput()
self.input_obs_id.on_select.connect(self.select_observation_by_id)
inputs_layout.addWidget(QLabel('Observation ID:'))
inputs_layout.addWidget(self.input_obs_id)

# Selected taxon/observation info
group_box = QGroupBox('Metadata source')
Expand All @@ -63,15 +59,11 @@ def __init__(self):
group_box.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Minimum)
top_section_layout.addWidget(group_box)
self.data_source_card = HorizontalLayout(group_box)

# Clear info when clearing an input field
self.input_obs_id.on_clear.connect(self.data_source_card.clear)
self.input_taxon_id.on_clear.connect(self.data_source_card.clear)
self.selected_taxon_id: Optional[int] = None
self.selected_observation_id: Optional[int] = None

# Image gallery
self.gallery = ImageGallery()
self.gallery.on_select_observation.connect(self.on_select_observation_tab)
self.gallery.on_select_taxon.connect(self.on_select_taxon_tab)
photo_layout.addWidget(self.gallery)

def run(self):
Expand All @@ -81,7 +73,7 @@ def run(self):
self.info('Select images to tag')
return

obs_id, taxon_id = self.input_obs_id.text(), self.input_taxon_id.text()
obs_id, taxon_id = self.selected_taxon_id, self.selected_observation_id
if not (obs_id or taxon_id):
self.info('Select either an observation or an organism to tag images with')
return
Expand Down Expand Up @@ -139,36 +131,71 @@ def paste(self):
# Check for IDs if an iNat URL was pasted
observation_id, taxon_id = get_ids_from_url(text)
if observation_id:
self.on_select_observation_id.emit(observation_id)
self.select_observation_by_id(observation_id)
elif taxon_id:
self.on_select_taxon_id.emit(taxon_id)
self.select_taxon_by_id(taxon_id)
# If not an iNat URL, check for valid image paths
else:
self.gallery.load_images(text.splitlines())

# Note: These methods duplicate "display_x_by_id" controller methods, but attempts at code reuse
# added too much spaghetti
def select_taxon_by_id(self, taxon_id: int):
"""Load a taxon by ID (pasted or directly entered)"""
if self.selected_taxon_id == taxon_id:
return

app = get_app()
logger.info(f'Loading taxon {taxon_id}')
future = app.threadpool.schedule(
lambda: app.client.taxa(taxon_id, locale=app.settings.locale),
priority=QThread.HighPriority,
)
future.on_result.connect(self.select_taxon)

def select_observation_by_id(self, observation_id: int):
"""Load an observation by ID (pasted or directly entered)"""
if self.selected_observation_id == observation_id:
return

app = get_app()
logger.info(f'Loading observation {observation_id}')
future = app.threadpool.schedule(
lambda: app.client.observations(observation_id, taxonomy=True),
priority=QThread.HighPriority,
)
future.on_result.connect(self.select_observation)

@Slot(Taxon)
def select_taxon(self, taxon: Taxon):
"""Update input info from a taxon object (loaded from Species tab)"""
if self.input_taxon_id.text() == str(taxon.id):
"""Update metadata info from a taxon object"""
if self.selected_taxon_id == taxon.id:
return

self.input_taxon_id.set_id(taxon.id)
self.selected_taxon_id = taxon.id
self.selected_observation_id = None
self.input_obs_id.clear()
self.input_taxon_id.clear()
self.data_source_card.clear()

card = TaxonInfoCard(taxon=taxon, delayed_load=False)
card.on_click.connect(self.on_select_taxon_tab)
card.on_click.connect(self.on_view_taxon_id)
self.data_source_card.addWidget(card)

@Slot(Observation)
def select_observation(self, observation: Observation):
"""Update input info from an observation object (loaded from Observations tab)"""
if self.input_obs_id.text() == str(observation.id):
"""Update input info from an observation object"""
if self.selected_observation_id == observation.id:
return

self.input_obs_id.set_id(observation.id)
self.input_taxon_id.set_id(observation.taxon.id)
self.selected_taxon_id = None
self.selected_observation_id = observation.id
self.input_obs_id.clear()
self.input_taxon_id.clear()
self.data_source_card.clear()

card = ObservationInfoCard(obs=observation, delayed_load=False)
card.on_click.connect(self.on_select_observation_tab)
card.on_click.connect(self.on_view_observation_id)
self.data_source_card.addWidget(card)

def info(self, message: str):
Expand Down
17 changes: 8 additions & 9 deletions naturtag/controllers/image_gallery.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,8 @@ class ImageGallery(BaseController):
"""Container for displaying local image thumbnails & info"""

on_load_images = Signal(list) #: New images have been loaded
on_select_taxon = Signal(int) #: A taxon was selected from context menu
on_select_observation = Signal(int) #: An observation was selected from context menu
on_view_taxon_id = Signal(int) #: A taxon was selected from context menu
on_view_observation_id = Signal(int) #: An observation was selected from context menu

def __init__(self):
super().__init__()
Expand All @@ -69,7 +69,6 @@ def __init__(self):
scroll_area.setWidgetResizable(True)
scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
scroll_area.setWidget(self.scroll_panel)
root.addLayout(self.flow_layout)
root.addWidget(scroll_area)

def clear(self):
Expand Down Expand Up @@ -128,8 +127,8 @@ def _bind_image_actions(self, thumbnail: 'ThumbnailCard'):
thumbnail.on_remove.connect(self.remove_image)
thumbnail.on_select.connect(self.select_image)
thumbnail.on_copy.connect(self.on_message)
thumbnail.context_menu.on_select_taxon.connect(self.on_select_taxon)
thumbnail.context_menu.on_select_observation.connect(self.on_select_observation)
thumbnail.context_menu.on_view_taxon_id.connect(self.on_view_taxon_id)
thumbnail.context_menu.on_view_observation_id.connect(self.on_view_observation_id)

def dragEnterEvent(self, event):
event.acceptProposedAction()
Expand Down Expand Up @@ -314,8 +313,8 @@ def set_pixmap_meta(self, pixmap_meta: tuple[QPixmap, MetaMetadata]):
class ThumbnailContextMenu(QMenu):
"""Context menu for local image thumbnails"""

on_select_taxon = Signal(int) #: A taxon was selected from context menu
on_select_observation = Signal(int) #: An observation was selected from context menu
on_view_taxon_id = Signal(int) #: A taxon was selected from context menu
on_view_observation_id = Signal(int) #: An observation was selected from context menu

def refresh_actions(self, thumbnail_card: ThumbnailCard):
"""Update menu actions based on the available metadata"""
Expand All @@ -328,7 +327,7 @@ def refresh_actions(self, thumbnail_card: ThumbnailCard):
text='View Taxon',
tooltip=f'View taxon {meta.taxon_id} in naturtag',
enabled=meta.has_taxon,
callback=lambda: self.on_select_taxon.emit(meta.taxon_id),
callback=lambda: self.on_view_taxon_id.emit(meta.taxon_id),
)
self._add_action(
parent=thumbnail_card,
Expand All @@ -344,7 +343,7 @@ def refresh_actions(self, thumbnail_card: ThumbnailCard):
text='View Observation',
tooltip=f'View observation {meta.observation_id} in naturtag',
enabled=meta.has_observation,
callback=lambda: self.on_select_observation.emit(meta.observation_id),
callback=lambda: self.on_view_observation_id.emit(meta.observation_id),
)
self._add_action(
parent=thumbnail_card,
Expand Down
Loading

0 comments on commit c566795

Please sign in to comment.