Compare commits
8
Commits
136eda40ba
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
77c9768948 | ||
|
|
a36fc2aa52 | ||
|
|
4978f4c49f | ||
|
|
534262a4c7 | ||
|
|
561ea57d3e | ||
|
|
1f3912ef57 | ||
|
|
c2f8063518 | ||
|
|
36c55c034c |
No files matched your search
@@ -0,0 +1,52 @@
|
||||
---
|
||||
name: Drie-split werkbalk
|
||||
overview: "Een derde editorindeling toevoegen: gereedschappen boven, opslaan/kopiëren/zoom onder, en Nieuw + Screencast in een verticale zijbalk links of rechts van het canvas. De zijde is te kiezen in Instellingen > Weergave en via rechtsklik op de werkbalk."
|
||||
todos:
|
||||
- id: settings-model
|
||||
content: EditorLayout.ThreeSplit en CaptureBarPosition toevoegen in AppSettings
|
||||
status: pending
|
||||
- id: editor-xaml
|
||||
content: CaptureBar/CaptureItems toevoegen in EditorWindow.xaml
|
||||
status: pending
|
||||
- id: editor-logic
|
||||
content: ApplyToolbarLayout, positie, lock, Ctrl+H, contextmenu en reset aanpassen in EditorWindow.xaml.cs
|
||||
status: pending
|
||||
- id: settings-ui
|
||||
content: Derde optie plus Links/Rechts in SettingsWindow (xaml + laden/opslaan/standaard)
|
||||
status: pending
|
||||
- id: verify
|
||||
content: Bouwen en alle indelingen handmatig testen
|
||||
status: pending
|
||||
isProject: false
|
||||
---
|
||||
|
||||
# Drie-split werkbalk met zijbalk links of rechts
|
||||
|
||||
## Gedrag
|
||||
- Nieuwe indeling "Drie-split": bovenbalk (gereedschappen), onderbalk (Opslaan, Uploaden, Kopiëren, zoom) en een smalle verticale zijbalk naast het canvas met **Nieuw** en **Screencast**.
|
||||
- De zijbalk staat links of rechts (standaard links). In de zijbalk staan alleen iconen; de bestaande logica in `UpdateActionLabels` verbergt labels al bij `Orientation.Vertical`.
|
||||
- De zijbalk staat in de `DockPanel` na `Toolbar`/`PropsBar`, zodat hij alleen naast het canvas loopt (zoals op positie 2 en 3 in je screenshot) en niet naast de boven- of onderbalk.
|
||||
|
||||
## Wijzigingen
|
||||
|
||||
**[src/Kniip/Settings/AppSettings.cs](src/Kniip/Settings/AppSettings.cs)**
|
||||
- `EditorLayout` uitbreiden met `ThreeSplit` (achteraan, zodat bestaande instellingen geldig blijven).
|
||||
- Nieuwe instelling `public Dock CaptureBarPosition { get; set; } = Dock.Left;` (alleen `Left`/`Right` gebruikt).
|
||||
|
||||
**[src/Kniip/Editor/EditorWindow.xaml](src/Kniip/Editor/EditorWindow.xaml)**
|
||||
- Nieuwe `Border x:Name="CaptureBar"` (standaard `Collapsed`) met `local:ToolbarPanel x:Name="CaptureItems"` (verticaal), met dezelfde `ContextMenuOpening`/`OrderChanged`-handlers als `ActionBar`. Plaats hem na `PropsBar` en vóór het canvas.
|
||||
|
||||
**[src/Kniip/Editor/EditorWindow.xaml.cs](src/Kniip/Editor/EditorWindow.xaml.cs)**
|
||||
- `IsSplitLayout` true maken voor zowel `Split` als `ThreeSplit` (beide gebruiken `SplitToolbarOrder` en `SplitBottomIds`), plus `IsThreeSplit`.
|
||||
- `SideIds = { "New", "Screencast" }`; in `ApplyToolbarLayout` gaan die items bij `ThreeSplit` naar `CaptureItems` in plaats van `ActionItems`. `CaptureItems.Children` ook leegmaken.
|
||||
- `CaptureBar.Visibility` instellen op dezelfde manier als `ActionBar` (ook in `ToggleToolbars`, Ctrl+H). `DockPanel.SetDock(CaptureBar, _settings.CaptureBarPosition)` en de rand aan de canvaszijde (`0,0,1,0` links, `1,0,0,0` rechts).
|
||||
- `AllToolbarItems` uitbreiden met `CaptureItems.Items`; `ApplyToolbarLock` ook voor `CaptureItems`/`CaptureBar`.
|
||||
- Contextmenu: in "Indeling" een derde optie "Drie-split: zijbalk met Nieuw en Screencast". Bij `ThreeSplit` een submenu "Zijbalk" met Links/Rechts (zelfde patroon als het bestaande "Positie"-menu). `Toolbar_ContextMenuOpening` het juiste paneel laten kiezen voor `CaptureBar`.
|
||||
- `ResetToolbar`: `CaptureBarPosition = Dock.Left`.
|
||||
|
||||
**[src/Kniip/Settings/SettingsWindow.xaml](src/Kniip/Settings/SettingsWindow.xaml)** en **[.xaml.cs](src/Kniip/Settings/SettingsWindow.xaml.cs)**
|
||||
- Derde radiobutton `LayoutThreeSplitRadio` "Drie-split: gereedschappen boven, opslaan onder, Nieuw en Screencast opzij", met daaronder (ingesprongen, alleen actief bij deze keuze) twee radiobuttons "Links" en "Rechts".
|
||||
- Laden (r. 104-105) en opslaan (r. 454) aanpassen voor drie waarden plus `CaptureBarPosition`; standaardinstellingen (r. ~481) ook terugzetten.
|
||||
|
||||
## Controle
|
||||
- Bouwen, editor openen, tussen alle drie de indelingen wisselen via Instellingen en rechtsklik. Controleren dat Links/Rechts direct werkt, Ctrl+H alle balken verbergt en terughaalt, en dat versleepte volgorde en verborgen knoppen bewaard blijven.
|
||||
@@ -0,0 +1,93 @@
|
||||
---
|
||||
name: Editor met tabbladen
|
||||
overview: Alle knipsels openen als tabblad in één kniip-editorvenster. Bij het sluiten van het venster kiest de gebruiker tussen alle tabbladen sluiten of ze behouden (venster verbergen), met een vinkje "Onthouden" en een bijbehorende instelling.
|
||||
todos:
|
||||
- id: view
|
||||
content: EditorWindow omzetten naar EditorView (UserControl) met TryClose, TabTitle, DirtyChanged en RefreshFromSettings
|
||||
status: pending
|
||||
- id: replace
|
||||
content: Plakken/slepen/openen/nieuw knipsel openen een nieuw tabblad in plaats van te vervangen
|
||||
status: pending
|
||||
- id: shell
|
||||
content: EditorShellWindow met TabControl, tabkoppen, sneltoetsen en sluitgedrag
|
||||
status: pending
|
||||
- id: dialog
|
||||
content: CloseEditorDialog met drie keuzes en vinkje Onthouden
|
||||
status: pending
|
||||
- id: settings
|
||||
content: EditorCloseBehavior in AppSettings en radioknoppen in SettingsWindow (Weergave)
|
||||
status: pending
|
||||
- id: app
|
||||
content: App.OpenEditor/OpenBlankEditor/tray/TryCloseAllWindows aanpassen aan enkel schilvenster
|
||||
status: pending
|
||||
- id: docs
|
||||
content: CHANGELOG en README bijwerken
|
||||
status: pending
|
||||
isProject: false
|
||||
---
|
||||
|
||||
# Editor met tabbladen in één venster
|
||||
|
||||
## Huidige situatie
|
||||
- [src/Kniip/App.xaml.cs](src/Kniip/App.xaml.cs) `OpenEditor` (r. 199) en `OpenBlankEditor` (r. 481) maken voor elk knipsel een nieuw `EditorWindow`.
|
||||
- [src/Kniip/Editor/EditorWindow.xaml.cs](src/Kniip/Editor/EditorWindow.xaml.cs) (~3200 regels) bevat alle documentstatus per venster (`_doc`, `_undo`, `_dirty`, zoom, hulplijnen, ...). Vervangacties (`ReplaceWith`, `DiscardAndStartNewSnip`, `DiscardAndStartBlank`, r. 2783-2900) openen een nieuw venster en sluiten het huidige; `OnClosing` (r. 2915) vraagt om te kopiëren bij niet-opgeslagen aantekeningen.
|
||||
|
||||
## Aanpak
|
||||
Bestaande editor wordt een `UserControl` per tabblad, zodat alle per-document-logica ongewijzigd blijft. Een nieuw schilvenster beheert de tabbladen.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
Capture[OnCaptureReceived / snip / bestand] --> AppOpen["App.OpenEditor"]
|
||||
AppOpen --> Shell["EditorShellWindow (enkel exemplaar)"]
|
||||
Shell --> Tab1["Tab: EditorView"]
|
||||
Shell --> Tab2["Tab: EditorView"]
|
||||
Shell -->|"venster sluiten"| Ask{"EditorCloseBehavior"}
|
||||
Ask -->|"CloseAll"| CloseTabs["Elk tabblad: TryClose met bestaande vraag"]
|
||||
Ask -->|"KeepTabs"| HideWin["Venster verbergen, tabs blijven in geheugen"]
|
||||
Ask -->|"Ask"| Dialog["CloseEditorDialog met vinkje Onthouden"]
|
||||
```
|
||||
|
||||
## Stappen
|
||||
|
||||
1. **EditorWindow omzetten naar `EditorView` (UserControl)**
|
||||
- Hernoem `EditorWindow.xaml(.cs)` naar `EditorView.xaml(.cs)`, root `<UserControl>`; venster-attributen (Title, grootte, Icon, `WindowStartupLocation`) verhuizen naar de schil. `AllowDrop`/drag-drop-handlers blijven op de view.
|
||||
- `MessageBox.Show(this, ...)`, `PickImageFile(this)` en de opslagdialoog krijgen `Window.GetWindow(this)` als eigenaar.
|
||||
- `WindowState = Minimized` (screencast r. 2802, OCR r. 2587) gaat via de schil (`OwnerShell.Minimize()` / herstel).
|
||||
- `OnClosing` wordt `public bool TryClose(bool force = false)`: dezelfde kopieervraag, `IgnoreCurrentClipboard` bij `_exported`, `SettingsStore.Save`. Afmelden van `Updates.AvailabilityChanged` gebeurt daar of in `Unloaded`.
|
||||
- `ZoomToFit` bij `Loaded`/`SizeChanged` blijft werken op de UserControl; `DpiChanged` via de schil doorgeven.
|
||||
- Nieuwe property `TabTitle` (vensternaam of "Knipsel HH:mm", "Leeg canvas") en event `DirtyChanged` voor een markering in de tabkop.
|
||||
- Methode `RefreshFromSettings()` (rulers, opslaanknop, werkbalkindeling), aangeroepen bij activeren van een tabblad, zodat wijzigingen in het ene tabblad ook in de andere doorkomen.
|
||||
|
||||
2. **Vervangacties openen voortaan een nieuw tabblad** (geen wis-bevestiging meer nodig)
|
||||
- `ReplaceWith` (plakken, slepen, Ctrl+O): `App.Instance.OpenEditor(image)` zonder het huidige tabblad te sluiten.
|
||||
- `DiscardAndStartNewSnip`: schil minimaliseren, `App.StartNewSnip()`; resultaat komt als nieuw tabblad.
|
||||
- `DiscardAndStartBlank` ("Wissen"): blijft het huidige tabblad vervangen door een leeg canvas, met de bestaande `ConfirmDiscard`.
|
||||
|
||||
3. **Nieuw `EditorShellWindow`** ([src/Kniip/Editor/EditorShellWindow.xaml](src/Kniip/Editor/EditorShellWindow.xaml))
|
||||
- `TabControl` met tabkop: titel, `*` bij niet-opgeslagen, sluitknop; middelklik sluit. Sneltoetsen Ctrl+W (tab sluiten), Ctrl+Tab / Ctrl+Shift+Tab (wisselen).
|
||||
- `AddTab(EditorView)` selecteert het nieuwe tabblad; laatste tabblad sluiten sluit het venster volledig (zonder vraag).
|
||||
- `OnClosing`: gedrag volgens `Settings.EditorCloseBehavior`; bij `KeepTabs` `e.Cancel = true` + `Hide()`. Parameter/flag `ForceCloseAll` voor afsluiten van de app en installeren van updates.
|
||||
- `ThemeManager.Attach(this)`, venstergrootte/-positie zoals nu.
|
||||
|
||||
4. **`CloseEditorDialog`** ([src/Kniip/Editor/CloseEditorDialog.xaml](src/Kniip/Editor/CloseEditorDialog.xaml))
|
||||
- Tekst "Er zijn N tabbladen open. Wat wil je doen?", knoppen "Alle tabbladen sluiten", "Tabbladen behouden", "Annuleren", vinkje "Onthouden".
|
||||
- Met vinkje: keuze opslaan in `EditorCloseBehavior` en `SettingsStore.Save`.
|
||||
|
||||
5. **Instellingen**
|
||||
- [src/Kniip/Settings/AppSettings.cs](src/Kniip/Settings/AppSettings.cs): `enum EditorCloseBehavior { Ask, CloseAll, KeepTabs }`, property `EditorCloseBehavior` (standaard `Ask`).
|
||||
- [src/Kniip/Settings/SettingsWindow.xaml](src/Kniip/Settings/SettingsWindow.xaml), tab "Weergave": groep "Editorvenster sluiten" met drie radioknoppen ("Elke keer vragen", "Alle tabbladen sluiten", "Tabbladen behouden"); inlezen in `LoadFrom`, wegschrijven in `Save_Click`.
|
||||
|
||||
6. **App-integratie** ([src/Kniip/App.xaml.cs](src/Kniip/App.xaml.cs))
|
||||
- Veld `_editorShell`; `EnsureEditorShell()` maakt hem aan of toont hem weer (na `Hide`), daarna `BringToFront`.
|
||||
- `OpenEditor` en `OpenBlankEditor` maken een `EditorView` en roepen `AddTab` aan; `placeLike` vervalt.
|
||||
- Tray: "Editor tonen" toont het verborgen venster met behouden tabbladen (alleen zichtbaar als die er zijn); de bestaande "open van klembord"-actie voegt een tabblad toe.
|
||||
- `TryCloseAllWindows` (r. 618): schil met `ForceCloseAll` sluiten; slaagt alleen als alle tabbladen gesloten zijn (`_editorShell == null`).
|
||||
|
||||
7. **Documentatie**: [CHANGELOG.md](CHANGELOG.md) en [README.md](README.md) bijwerken.
|
||||
|
||||
## Testpunten
|
||||
- Twee screenshots achter elkaar: één venster, twee tabbladen.
|
||||
- Venster sluiten met elke keuze, met/zonder "Onthouden"; instelling daarna terug te zien en aan te passen in Instellingen.
|
||||
- Behouden: volgende screenshot of tray toont het venster met alle oude tabbladen.
|
||||
- Tabblad met niet-opgeslagen aantekeningen sluiten geeft de bestaande kopieervraag; Annuleren breekt "Alle sluiten" af.
|
||||
- Afsluiten via tray en update-installatie sluiten alles, ook bij instelling "Behouden".
|
||||
@@ -0,0 +1,33 @@
|
||||
---
|
||||
name: Mapknop bij liniaal
|
||||
overview: "Een icoonknop in de liniaalhoek van de editor opent een menu met twee acties: de screenshotmap en de screencastmap openen."
|
||||
todos:
|
||||
- id: corner-button
|
||||
content: RulerCorner vervangen door een icoonknop van 20×20 die alleen zichtbaar is met de linialen
|
||||
status: pending
|
||||
- id: folder-menu
|
||||
content: Klik opent een menu eronder met Screenshotmap openen en Screencastmap openen, via HistoryActions.OpenFolder
|
||||
status: pending
|
||||
isProject: false
|
||||
---
|
||||
|
||||
# Mapknop in de liniaalhoek
|
||||
|
||||
In de editor komt in het vak links van de linialen één knop, alleen een mapicoon. Een klik opent daaronder twee items: **Screenshotmap openen** en **Screencastmap openen**. Rechtsklik op die hoek blijft het bestaande menu (hulplijnen wissen, linialen verbergen).
|
||||
|
||||
De knop zit in hetzelfde vak als de linialen en is alleen zichtbaar als de linialen aan staan.
|
||||
|
||||
## Welke map
|
||||
|
||||
Zelfde volgorde als de opslag in de instellingen:
|
||||
|
||||
- Screenshotmap: de knipselmap uit **Opslaan** als die is ingesteld, anders de laatst gebruikte map, anders de Windows-map Schermafbeeldingen, anders Afbeeldingen.
|
||||
- Screencastmap: de screencastmap uit **Opslaan**, anders de laatst gebruikte, anders `Video's\kniip`.
|
||||
|
||||
Openen gaat via [`HistoryActions.OpenFolder`](src/Kniip/History/HistoryActions.cs), die de map aanmaakt als ze nog niet bestaat en daarna Verkenner opent.
|
||||
|
||||
## Wijziging
|
||||
|
||||
In [`EditorWindow.xaml`](src/Kniip/Editor/EditorWindow.xaml) wordt het lege vak `RulerCorner` (rond regel 750) een platte knop van 20×20, met het mapicoon (`Segoe Fluent Icons`, `\uE8B7`) en tooltip **Map openen**. Geen tekst op de knop.
|
||||
|
||||
In [`EditorWindow.xaml.cs`](src/Kniip/Editor/EditorWindow.xaml.cs) opent de klik een `ContextMenu` met `Placement=Bottom` onder die knop, met de twee items via de bestaande `MenuItem`-helper. De paden komen uit `_settings.SnipSaveFolder` / `LastSaveFolder` en `_settings.VideoSaveFolder` / `LastVideoFolder`, met de fallbacks hierboven (`CaptureCoordinator.ScreenshotsFolder` voor screenshots).
|
||||
@@ -0,0 +1,85 @@
|
||||
---
|
||||
name: Mappenknop in editor
|
||||
overview: "Eén icoonknop met een mapicoon links van de knop Linialen in de editorwerkbalk. Een klik opent een menu met twee items: Open Screenshot map en Open Screencast map. Ze openen dezelfde mappen als het systeemvakmenu."
|
||||
todos:
|
||||
- id: app-folders
|
||||
content: SnipFolder/VideoFolder in App.xaml.cs, TrayFolders() daarop laten leunen
|
||||
status: pending
|
||||
- id: xaml-button
|
||||
content: Mappenknop (ItemId Folders) tussen Sep4 en RulersToggle in EditorWindow.xaml
|
||||
status: pending
|
||||
- id: toolbar-lists
|
||||
content: Folders in DefaultSplitOrder voor Rulers en in SplitBottomIds
|
||||
status: pending
|
||||
- id: menu
|
||||
content: Folders_Click met ContextMenu met twee items in EditorWindow.xaml.cs
|
||||
status: pending
|
||||
- id: changelog
|
||||
content: CHANGELOG 2.1.0 aanvullen
|
||||
status: pending
|
||||
isProject: false
|
||||
---
|
||||
|
||||
# Mappenknop in de editor
|
||||
|
||||
## Aanpak
|
||||
|
||||
Het systeemvakmenu kent al mappen voor "Knipsels" en "Screencasts" via `TrayFolders()` in [src/Kniip/App.xaml.cs](src/Kniip/App.xaml.cs) (regel 565-578). Ze worden geopend met `HistoryActions.OpenFolder(path)`, dat de map zo nodig eerst aanmaakt. De nieuwe knop gebruikt dezelfde mappen, zodat de editor en het systeemvak altijd hetzelfde openen. Het menu werkt net als het pijltje bij Opslaan (`SaveMenu_MouseDown`): een WPF-`ContextMenu` gemaakt met de bestaande helper `MenuItem(header, glyph, gesture, action)`.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
FoldersButton["Mappenknop"] --> Menu[ContextMenu]
|
||||
Menu --> Shot["Open Screenshot map"]
|
||||
Menu --> Cast["Open Screencast map"]
|
||||
Shot --> SnipFolder["App.SnipFolder"]
|
||||
Cast --> VideoFolder["App.VideoFolder"]
|
||||
SnipFolder --> OpenFolder["HistoryActions.OpenFolder"]
|
||||
VideoFolder --> OpenFolder
|
||||
```
|
||||
|
||||
## Wijzigingen
|
||||
|
||||
**[src/Kniip/App.xaml.cs](src/Kniip/App.xaml.cs)**
|
||||
- Haal de mapkeuze uit `TrayFolders()` en maak er twee publieke eigenschappen van:
|
||||
- `SnipFolder`: `SnipSaveFolder`, anders `LastSaveFolder`, anders `MyPictures`. Dit is de keuze "Knipsels".
|
||||
- `VideoFolder`: `VideoSaveFolder`, anders `LastVideoFolder`, anders `MyVideos\kniip`. De laatste map hoeft nog niet te bestaan, want `OpenFolder` maakt hem aan. Daardoor staat "Screencasts" ook in het systeemvakmenu altijd aan.
|
||||
- `TrayFolders()` gebruikt deze twee eigenschappen.
|
||||
|
||||
**[src/Kniip/Editor/EditorWindow.xaml](src/Kniip/Editor/EditorWindow.xaml)**
|
||||
- Nieuwe knop tussen `Sep4` en `RulersToggle` (regel ~476), in dezelfde stijl als de buren:
|
||||
|
||||
```xml
|
||||
<Button x:Name="FoldersButton" local:ToolbarPanel.ItemId="Folders" local:ToolbarPanel.Title="Mappen openen"
|
||||
Style="{StaticResource FlatButton}" ToolTip="Screenshot- of screencastmap openen" Click="Folders_Click">
|
||||
<TextBlock Text="" Style="{StaticResource IconText}" />
|
||||
</Button>
|
||||
```
|
||||
|
||||
**[src/Kniip/Editor/EditorWindow.xaml.cs](src/Kniip/Editor/EditorWindow.xaml.cs)**
|
||||
- In `DefaultSplitOrder` (regel 50-55) `"Folders"` invoegen tussen `"Spacer2"` en `"Rulers"`, en `"Folders"` toevoegen aan `SplitBottomIds`. Dan staat de knop in de gesplitste indeling rechtsonder, direct links van Linialen.
|
||||
- `Folders_Click` naast `SaveMenu_MouseDown`:
|
||||
|
||||
```csharp
|
||||
var menu = new ContextMenu { PlacementTarget = FoldersButton, Placement = PlacementMode.Bottom };
|
||||
menu.Items.Add(MenuItem("Open Screenshot map", "\uE91B", "", () => HistoryActions.OpenFolder(App.Instance.SnipFolder)));
|
||||
menu.Items.Add(MenuItem("Open Screencast map", "\uE714", "", () => HistoryActions.OpenFolder(App.Instance.VideoFolder)));
|
||||
menu.IsOpen = true;
|
||||
```
|
||||
|
||||
WPF klapt het menu vanzelf omhoog als er onder de knop geen ruimte is, zoals bij een werkbalk onderaan.
|
||||
|
||||
Bestaande gebruikers zien de knop meteen. `MergeToolbarOrder` zet nieuwe ID's achter hun standaardvoorganger: `Sep4` bij één werkbalk en `Spacer2` in de gesplitste indeling. In beide gevallen komt de knop dus direct links van Linialen. Hij is te verslepen en te verbergen via het bestaande werkbalkmenu.
|
||||
|
||||
**[CHANGELOG.md](CHANGELOG.md)**
|
||||
- Onder 2.1.0 > Nieuw een item toevoegen:
|
||||
|
||||
```markdown
|
||||
- **Mappen openen.**\
|
||||
Links van de knop Linialen staat een knop met een mapicoon. Klik erop en kies **Open Screenshot map** of **Open Screencast map**. Je krijgt dezelfde mappen als in het systeemvakmenu: de opslagmap uit de instellingen, anders de map waarin je het laatst hebt opgeslagen.
|
||||
```
|
||||
|
||||
De versie blijft 2.1.0, want die is nog niet uitgebracht.
|
||||
|
||||
## Niet in scope
|
||||
- Geen extra mapknop in het screencastvenster. Daar staat na het opslaan al "Map openen".
|
||||
- Geen item voor de Windows-map Schermafbeeldingen. Die blijft in het systeemvakmenu staan.
|
||||
@@ -0,0 +1,90 @@
|
||||
---
|
||||
name: Meertaligheid kniip
|
||||
overview: kniip meertalig maken (Nederlands, Engels, Duits, Frans, Spaans, Italiaans, Portugees) met een taalinstelling die standaard de Windows-taal volgt en direct wisselt zonder herstart, net als de kleurmodus. Daarnaast de installer en de README vertalen.
|
||||
todos:
|
||||
- id: infra
|
||||
content: LanguageManager, AppLanguage-instelling, Strings.*.xaml-skelet en opstart-aanroep
|
||||
status: pending
|
||||
- id: settings-ui
|
||||
content: Taalkeuze op tab Weergave in SettingsWindow met direct wisselen
|
||||
status: pending
|
||||
- id: xaml
|
||||
content: Alle XAML-teksten omzetten naar DynamicResource (nl-bron)
|
||||
status: pending
|
||||
- id: csharp
|
||||
content: C#-teksten omzetten naar LanguageManager.T, inclusief tray en verversen bij Changed
|
||||
status: pending
|
||||
- id: translate
|
||||
content: Vertalingen en, de, fr, es, it, pt invullen en controlescript voor sleutels in tools/
|
||||
status: pending
|
||||
- id: installer
|
||||
content: "Inno Setup: zeven talen, CustomMessages, eerste taal doorgeven aan de app"
|
||||
status: pending
|
||||
- id: docs
|
||||
content: README in zeven talen, CHANGELOG-entry
|
||||
status: pending
|
||||
- id: verify
|
||||
content: Bouwen, elke taal doorlopen, breedtes van lange teksten (de, fr, es, it, pt) nalopen
|
||||
status: pending
|
||||
isProject: false
|
||||
---
|
||||
|
||||
# Meertaligheid kniip (nl, en, de, fr, es, it, pt)
|
||||
|
||||
## Aanpak
|
||||
|
||||
Zelfde patroon als [src/Kniip/Infrastructure/ThemeManager.cs](src/Kniip/Infrastructure/ThemeManager.cs): per taal een `ResourceDictionary` met strings, die in `Application.Current.Resources.MergedDictionaries` wordt gewisseld. XAML gebruikt `DynamicResource`, dus alle open vensters wisselen direct mee.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
Settings["AppSettings.Language"] --> LangMgr["LanguageManager.Apply"]
|
||||
LangMgr --> Dict["Strings.xx.xaml in MergedDictionaries"]
|
||||
Dict --> Xaml["XAML: DynamicResource"]
|
||||
LangMgr --> Changed["LanguageManager.Changed"]
|
||||
Changed --> CodeTexts["C#-teksten, tray, open menu's verversen"]
|
||||
LangMgr --> Culture["CurrentUICulture en CurrentCulture"]
|
||||
```
|
||||
|
||||
## Onderdelen
|
||||
|
||||
**1. Taalbestanden** in de nieuwe map `src/Kniip/Languages/`:
|
||||
- `Strings.nl.xaml` (bron, huidige teksten), `Strings.en.xaml`, `Strings.de.xaml`, `Strings.fr.xaml`, `Strings.es.xaml`, `Strings.it.xaml`, `Strings.pt.xaml`
|
||||
- Portugees is Braziliaans Portugees (de grootste groep gebruikers) en wordt ook gebruikt voor Windows in pt-PT
|
||||
- Sleutels in de vorm `Settings.Hotkeys.Title`, `Editor.Menu.Undo`, `Tray.PauseCapture`; teksten met variabelen gebruiken `{0}` voor `string.Format`
|
||||
- Een ontbrekende sleutel in een taal valt terug op Engels (Engels wordt onder de gekozen taal gemerged)
|
||||
|
||||
**2. `LanguageManager`** (nieuw, `src/Kniip/Infrastructure/LanguageManager.cs`):
|
||||
- `Apply(AppLanguage)`: wisselt de dictionary, zet `CultureInfo` (datums, getallen) en het `Language`-attribuut van open vensters, en roept daarna `Changed` aan
|
||||
- `T(string key)` en `T(string key, params object[] args)` voor C#-code
|
||||
- Bij `System` de Windows-UI-taal (`CultureInfo.InstalledUICulture`) nemen; staat die niet in de lijst, dan Engels
|
||||
|
||||
**3. Instelling** in [src/Kniip/Settings/AppSettings.cs](src/Kniip/Settings/AppSettings.cs):
|
||||
- `enum AppLanguage { System, Dutch, English, German, French, Spanish, Italian, Portuguese }`, met `public AppLanguage Language { get; set; } = AppLanguage.System;`
|
||||
- In [src/Kniip/Settings/SettingsWindow.xaml](src/Kniip/Settings/SettingsWindow.xaml), tab Weergave, een kopje "Taal" boven Kleurmodus met een ComboBox. Taalnamen staan altijd in de eigen taal (Nederlands, English, Deutsch, Français, Español, Italiano, Português), en een wissel geldt direct, net als bij de kleurmodus
|
||||
- Aanroep van `LanguageManager.Apply` bij het opstarten in [src/Kniip/App.xaml.cs](src/Kniip/App.xaml.cs), vóór `ThemeManager.Apply` en vóór het eerste venster
|
||||
|
||||
**4. XAML omzetten**: elke `Text`, `Content`, `ToolTip`, `Header` en `Title` wordt `{DynamicResource ...}`. Op volgorde van omvang:
|
||||
- `SettingsWindow.xaml` (ongeveer 110 teksten), `EditorWindow.xaml` (ongeveer 100), `ScreencastWindow.xaml`, `HistoryWindow.xaml`, `RecordingToolbar.xaml`, `SnipToast.xaml`, `CountdownWindow.xaml`, `RegionSelectorWindow.xaml`, `RegionCaptureWindow.xaml`, `ColorPickerButton.xaml`, `EmojiPickerButton.xaml`, `App.xaml`
|
||||
|
||||
**5. C#-teksten omzetten** naar `LanguageManager.T(...)`:
|
||||
- Menu's die bij openen worden opgebouwd (contextmenu's in `EditorWindow.xaml.cs`, werkbalkmenu) zijn daarmee meteen goed
|
||||
- Teksten die langer blijven staan (bijvoorbeeld `HintText`, statusteksten, labels in `SettingsWindow.xaml.cs` en `ScreencastWindow.xaml.cs`) krijgen `SetResourceReference` of verversen bij `LanguageManager.Changed`
|
||||
- Tray ([src/Kniip/Infrastructure/TrayIcon.cs](src/Kniip/Infrastructure/TrayIcon.cs), WinForms) bouwt de menu-teksten opnieuw op bij `Changed`
|
||||
- Overige bestanden: `App.xaml.cs`, `HistoryActions.cs`, `HistoryWindow.xaml.cs`, `UploaderImporter.cs`, `ZiplineUploader.cs`, `RecordingController.cs`, `UpdateInstaller.cs`, `UpdateChecker.cs`, `ShellIntegration.cs`, `HotkeyGesture.cs` (namen zoals "Geen"), `EmojiCatalog.cs` (categorienamen), `MarkdownRenderer.cs`, `RulerControl.cs`, `VideoExporter.cs`, `GlobalHotkeyHook.cs`, `AppIdentity.cs`
|
||||
- Standaardbestandsnamen "Knipsel" en "Screencast" volgen de taal (Knipsel, Snip, Ausschnitt, Capture, Recorte, Ritaglio, Recorte). Een eigen naamsjabloon blijft ongewijzigd
|
||||
- Release notes van GitHub blijven in de taal waarin ze geschreven zijn
|
||||
|
||||
**6. Project**: in [src/Kniip/Kniip.csproj](src/Kniip/Kniip.csproj) wordt `SatelliteResourceLanguages` `nl;en;de;fr;es;it;pt-BR` (voor de WPF-systeemteksten), en de `Description` wordt Engels.
|
||||
|
||||
**7. Installer** ([installer/Kniip.iss](installer/Kniip.iss)):
|
||||
- In `[Languages]` komen `dutch`, `english` (`Default.isl`), `german`, `french`, `spanish`, `italian` en `brazilianportuguese` (`BrazilianPortuguese.isl`), met `ShowLanguageDialog=auto`
|
||||
- De eigen teksten (taken, "kniip nu starten", groepnamen) gaan naar `[CustomMessages]` per taal en worden aangeroepen met `{cm:...}`
|
||||
- De gekozen taal geeft de installer als eerste taal door aan de app, via `HKCU\Software\kniip\Language`, alleen als er nog geen instelling bestaat. Komt dit in de praktijk niet goed over, dan blijft `System` de standaard
|
||||
|
||||
**8. Documentatie**:
|
||||
- `README.md` wordt Engels (internationaal standaard) met taallinks bovenaan; daarnaast `README.nl.md` (huidige tekst), `README.de.md`, `README.fr.md`, `README.es.md`, `README.it.md` en `README.pt.md`
|
||||
- `CHANGELOG.md`: vanaf nu in het Engels; de bestaande geschiedenis blijft Nederlands. Een nieuwe entry voor deze versie meldt de taalondersteuning
|
||||
|
||||
## Controle
|
||||
- Bouwen met `build.ps1`, daarna elke taal instellen en alle vensters doorlopen op ontbrekende sleutels (die zijn zichtbaar als lege tekst) en op afgekapte lange teksten (vooral Duits, Frans, Spaans, Italiaans en Portugees) in vaste breedtes (bijvoorbeeld `Width="150"` in de instellingen). Waar nodig wordt de breedte aangepast
|
||||
- Een klein script in `tools/` vergelijkt de sleutels van de zeven taalbestanden met elkaar
|
||||
@@ -0,0 +1,78 @@
|
||||
---
|
||||
name: Printknop in editor
|
||||
overview: Een icoonknop Printen links van het tandwiel in de editorwerkbalk (plus Ctrl+P) die het knipsel met annotaties via het standaard Windows-printvenster afdrukt, passend op de pagina.
|
||||
todos:
|
||||
- id: xaml-button
|
||||
content: Icoonknop Printen (ItemId Print, FlatButton) links van het tandwiel in EditorWindow.xaml
|
||||
status: completed
|
||||
- id: toolbar-lists
|
||||
content: Print invoegen voor Settings in DefaultSplitOrder
|
||||
status: completed
|
||||
- id: print-method
|
||||
content: Print() + Print_Click + Ctrl+P in EditorWindow.xaml.cs (PrintDialog, passend op pagina)
|
||||
status: completed
|
||||
- id: docs
|
||||
content: CHANGELOG-sectie 2.1.0 en README (Delen, build-voorbeeld) bijwerken
|
||||
status: completed
|
||||
- id: version
|
||||
content: Versie naar 2.1.0 in Kniip.csproj, installer/Kniip.iss en build.ps1
|
||||
status: completed
|
||||
isProject: false
|
||||
---
|
||||
|
||||
# Printknop in de editor
|
||||
|
||||
## Aanpak
|
||||
|
||||
Het knipsel wordt al plat gemaakt voor Opslaan en Kopiëren via `DocumentRenderer.Export(_doc)`. Printen gebruikt dezelfde afbeelding en stuurt die naar de WPF-`PrintDialog` (`System.Windows.Controls`, geen extra package nodig).
|
||||
|
||||
## Wijzigingen
|
||||
|
||||
**[src/Kniip/Editor/EditorWindow.xaml](src/Kniip/Editor/EditorWindow.xaml)**
|
||||
- Nieuwe icoonknop direct links van het tandwiel: tussen `Sep5` en `SettingsButton` (regel ~495), in dezelfde stijl als Instellingen:
|
||||
|
||||
```xml
|
||||
<Button local:ToolbarPanel.ItemId="Print" local:ToolbarPanel.Title="Printen"
|
||||
Style="{StaticResource FlatButton}" ToolTip="Printen (Ctrl+P)" Click="Print_Click">
|
||||
<TextBlock Text="" Style="{StaticResource IconText}" />
|
||||
</Button>
|
||||
```
|
||||
|
||||
**[src/Kniip/Editor/EditorWindow.xaml.cs](src/Kniip/Editor/EditorWindow.xaml.cs)**
|
||||
- In `DefaultSplitOrder` (regel 50-55) `"Print"` invoegen tussen `"SplitSpacer2"` en `"Settings"`, zodat de knop in de gesplitste indeling rechtsboven links van het tandwiel staat. Niet toevoegen aan `SplitBottomIds`.
|
||||
- Geen label, dus niets toevoegen aan `UpdateActionLabels`.
|
||||
- `Print_Click` naast `Save_Click`, en `case Key.P: Print();` in de Ctrl-sneltoetsen (regel ~2916). Ctrl+P is nog vrij.
|
||||
- Nieuwe methode `Print()`:
|
||||
- `CommitTextEdit()`, daarna `DocumentRenderer.Export(_doc)`
|
||||
- `new PrintDialog()`; als de gebruiker annuleert, gebeurt er niets
|
||||
- Afbeelding in een `DrawingVisual` tekenen, gecentreerd in het afdrukbare gebied (`PrintableAreaWidth/Height`), met behouden verhouding en alleen verkleind als het knipsel niet past (kleine knipsels worden niet uitvergroot)
|
||||
- `dialog.PrintVisual(visual, "kniip - knipsel")`, daarna `ShowTemporaryHint("Naar de printer gestuurd.")`
|
||||
- Fouten tonen via `MessageBox` op dezelfde manier als `WriteTo` ("Printen is mislukt: ...")
|
||||
|
||||
Bestaande gebruikers krijgen de knop automatisch te zien: `MergeToolbarOrder` voegt nieuwe ID's in na hun standaardvoorganger (`Sep5` bij één werkbalk, `SplitSpacer2` in de gesplitste indeling), dus direct links van het tandwiel. De knop is te verslepen en te verbergen via het bestaande werkbalkmenu.
|
||||
|
||||
**[CHANGELOG.md](CHANGELOG.md)**
|
||||
- Nieuwe sectie bovenaan, in de stijl van 2.0.0:
|
||||
|
||||
```markdown
|
||||
## 2.1.0 - 2026-11-11
|
||||
|
||||
### Nieuw
|
||||
|
||||
- **Printen.**\
|
||||
Druk een knipsel met je aantekeningen af. De knop met het printericoon staat links van het tandwiel, of druk op Ctrl+P. Je kiest de printer en de afdrukstand in het printvenster van Windows. Het knipsel komt in het midden van de pagina; is het te groot, dan wordt het verkleind.
|
||||
```
|
||||
|
||||
**Versienummer naar 2.1.0**
|
||||
- [src/Kniip/Kniip.csproj](src/Kniip/Kniip.csproj) regel 21: `<Version>2.1.0</Version>`
|
||||
- [installer/Kniip.iss](installer/Kniip.iss) regel 8: `#define AppVersion "2.1.0"`
|
||||
- [build.ps1](build.ps1) regel 15: `[string]$Version = "2.1.0",`
|
||||
|
||||
**[README.md](README.md)**
|
||||
- Bij "Delen" (regel 28) printen toevoegen: "Opslaan, kopiëren naar het klembord, printen of uploaden ...".
|
||||
- Regel 83: `.\build.ps1 # versie 2.1.0`.
|
||||
- De downloadtabel (regel 37) blijft op 2.0.0 tot de release van 2.1.0 echt op git.hibbit.net staat (stap 4 van "Een nieuwe versie publiceren").
|
||||
|
||||
## Niet in scope
|
||||
- Geen eigen afdrukvoorbeeld of pagina-instellingen; afdrukstand en printer kies je in het Windows-printvenster.
|
||||
- Geen printknop in het screencastvenster.
|
||||
@@ -0,0 +1,94 @@
|
||||
---
|
||||
name: Tekstballon en -wolk
|
||||
overview: Het gereedschap "Tekst in kader" krijgt een vormkeuze (kader, ballon, ovale ballon, wolk). Ballon en wolk hebben een staart met een sleepbare greep op de punt, zodat je ermee iets kunt aanwijzen.
|
||||
todos:
|
||||
- id: model
|
||||
content: TextShape enum, StyleSet.TextShape en StyleProps.TextShape toevoegen in StyleSet.cs
|
||||
status: completed
|
||||
- id: geometry
|
||||
content: "TextAnnotation: TailTip, BuildShape (kader/ballon/ovaal/wolk), Bounds/HitTest/Move/handles"
|
||||
status: completed
|
||||
- id: resize
|
||||
content: "TextAnnotation: BoxWidth/BoxHeight, Box/TextArea, 8 formaatgrepen, woordafbreking, dubbelklik = automatisch; TextBox-editor op TextArea met vaste breedte"
|
||||
status: completed
|
||||
- id: ui
|
||||
content: Vorm-keuzelijst + template in EditorWindow.xaml, wiring in UpdatePropsPanel en TextShape_Changed
|
||||
status: completed
|
||||
- id: create
|
||||
content: Standaard staartpunt bij nieuwe tekst; omtrek+staart zichtbaar tijdens bewerken
|
||||
status: completed
|
||||
- id: verify
|
||||
content: "Bouwen en handmatig testen: vormen wisselen, staart slepen, verplaatsen, kopiëren/plakken, export"
|
||||
status: completed
|
||||
isProject: false
|
||||
---
|
||||
|
||||
# Tekstballon en -wolk voor "Tekst in kader"
|
||||
|
||||
## Gedrag
|
||||
- In de eigenschappenbalk van het tekstgereedschap (en van een geselecteerd tekstobject) komt een nieuwe keuzelijst **Vorm**:
|
||||
- **Kader**: zoals nu, zonder staart.
|
||||
- **Ballon**: een rechthoek met een spitse staart. De bestaande keuze voor afgeronde hoeken blijft hierbij werken.
|
||||
- **Ovale ballon**: een ovaal om de tekst, met een spitse staart.
|
||||
- **Wolk**: een bolle wolkrand met als staart drie steeds kleinere rondjes (een gedachtewolk).
|
||||
- De punt van de staart is een greep. Je kunt die met het selectiegereedschap of het tekstgereedschap verslepen naar wat je wilt aanwijzen.
|
||||
- Als je het hele object verplaatst, schuift de staartpunt mee. Als de tekst langer wordt, blijft de punt op dezelfde plek staan, dus hij blijft naar hetzelfde wijzen.
|
||||
- Een nieuw object krijgt de staartpunt standaard schuin linksonder het kader.
|
||||
- Ligt de punt binnen de vorm, dan wordt er geen staart getekend.
|
||||
- De rand, vulling, stippellijn en schaduw lopen als één doorlopende omtrek om de vorm en de staart heen.
|
||||
- **Formaat aanpassen** (geldt voor alle vormen, ook Kader): een geselecteerd tekstobject krijgt 8 formaatgrepen rond de vorm, net als bij een rechthoek. Zodra je die versleept, krijgt het kader een vaste breedte en loopt de tekst daarbinnen door naar de volgende regel.
|
||||
- De hoogte die je instelt is een minimum. Past de tekst er niet in, dan groeit het kader naar beneden mee.
|
||||
- Is het kader hoger dan de tekst, dan staat de tekst verticaal in het midden.
|
||||
- Een nieuw tekstobject begint zoals nu, met een automatische grootte. Dubbelklik op een formaatgreep zet het kader terug naar automatisch.
|
||||
- De staartpunt blijft op zijn plek als je het formaat aanpast.
|
||||
|
||||
## Technische aanpak
|
||||
|
||||
**1. Datamodel** in [src/Kniip/Editor/StyleSet.cs](src/Kniip/Editor/StyleSet.cs)
|
||||
- Nieuwe enum `TextShape { Frame, Balloon, Oval, Cloud }`.
|
||||
- Nieuwe stijleigenschap `TextShape TextShape` (standaard `Frame`). Oude instellingen blijven daardoor werken.
|
||||
- Nieuwe vlag `StyleProps.TextShape`, toe te voegen aan `PropsFor(ToolKind.Text)`.
|
||||
- De gekozen vorm wordt per gereedschap onthouden via de bestaande `ToolStyles`.
|
||||
|
||||
**2. Vorm tekenen** in [src/Kniip/Editor/Annotations/TextAnnotation.cs](src/Kniip/Editor/Annotations/TextAnnotation.cs)
|
||||
- Nieuwe eigenschap `Point? TailTip`. Zolang die leeg is, wordt de standaardpositie onder het kader berekend.
|
||||
- Nieuwe hulpfunctie `BuildShape(Rect box)` die de `Geometry` opbouwt:
|
||||
- **Kader**: de huidige `RectangleGeometry`.
|
||||
- **Ballon** (zoals in het voorbeeld met het rode kader dat naar het Google-logo wijst): een afgeronde rechthoek, samengevoegd met een spitse driehoekige staart via `Geometry.Combine(..., GeometryCombineMode.Union)`. De voet van de staart zit op de zijde die naar de punt kijkt (links, rechts, boven of onder, afhankelijk van waar de punt ligt). Hij staat ongeveer midden op die zijde en schuift mee richting de punt, maar blijft buiten de afgeronde hoeken. De voet is ongeveer 25% van die zijde breed en de staart loopt spits uit naar de greep.
|
||||
- **Ovaal**: een `EllipseGeometry` die zo veel ruimer is dan het tekstvlak (factor ongeveer √2) dat de tekst erin past, plus dezelfde staart.
|
||||
- **Wolk**: een vereniging van cirkels langs de omtrek van een ovaal, plus het ovaal zelf. De staart bestaat uit drie losse rondjes van de rand naar de punt, die steeds kleiner worden.
|
||||
- Nieuwe eigenschappen `double? BoxWidth` en `double? BoxHeight`. Is `null` een automatische grootte, zoals nu.
|
||||
- Nieuwe hulpfuncties `Box` (de rechthoek van de vorm zonder staart, met `Position` linksboven) en `TextArea` (het tekstvlak daarbinnen):
|
||||
- Bij Kader en Ballon is `TextArea` gelijk aan `Box` min de `Padding`.
|
||||
- Bij Ovaal en Wolk springt `TextArea` extra in, zodat de tekst binnen de ronding blijft.
|
||||
- Bij een vaste breedte krijgt `FormattedText.MaxTextWidth` de breedte van `TextArea`, zodat de tekst op woorden afbreekt. De hoogte wordt `max(BoxHeight, tekst + padding)`.
|
||||
- Bij een automatische grootte wordt `Box` uit de tekstmaat berekend, opgeblazen naar de vorm.
|
||||
- `Render` gebruikt `BuildShape(Box)` in plaats van de vaste rechthoek, en tekent de tekst verticaal gecentreerd in `TextArea`.
|
||||
- `Bounds` en `HitTest` nemen de buitengrenzen van de geometrie mee, inclusief de staart.
|
||||
- `GetHandles()` geeft eerst de 8 formaatgrepen van `Box` (zelfde volgorde als in [RectAnnotation.cs](src/Kniip/Editor/Annotations/RectAnnotation.cs)) en daarna, behalve bij Kader, de staartpunt (index 8).
|
||||
- `BeginHandleDrag` onthoudt de oorspronkelijke `Box`.
|
||||
- `MoveHandle` voor index 0 tot en met 7 rekent de nieuwe rechthoek uit zoals `RectAnnotation.MoveHandle`, via de bestaande `RectAnnotation.FromAnchor`. Daarna zet het `Position`, `BoxWidth` en `BoxHeight`, met een minimale breedte van ongeveer twee tekens plus padding.
|
||||
- Index 8 past `TailTip` aan.
|
||||
- `RemoveHandle` (dubbelklik) op een formaatgreep zet `BoxWidth` en `BoxHeight` terug op `null`.
|
||||
- `Move` verschuift ook `TailTip`.
|
||||
|
||||
**3. Editor** in [src/Kniip/Editor/EditorWindow.xaml](src/Kniip/Editor/EditorWindow.xaml) en [src/Kniip/Editor/EditorWindow.xaml.cs](src/Kniip/Editor/EditorWindow.xaml.cs)
|
||||
- Een `TextShapeGroup` met `TextShapeCombo` en een `TextShapeTemplate` (klein vormicoontje met een label), naar het voorbeeld van `CursorKindTemplate` en `CursorKindCombo`.
|
||||
- `TextShapeValues` als `ItemsSource`. In `UpdatePropsPanel` worden zichtbaarheid en selectie gezet. Een handler `TextShape_Changed` roept `ChangeStyle("textshape", s => s.TextShape = shape)` aan.
|
||||
- Bij het aanmaken van een nieuwe tekst (rond regel 842) wordt de standaard `TailTip` vastgezet. Het verslepen van de greep loopt via het bestaande generieke handle-mechanisme (rond regel 778), daar hoeft niets aan te veranderen.
|
||||
- `BeginTextEdit` en `ApplyTextEditorStyle` zetten de `TextBox` op `TextArea` in plaats van op `Position`. Bij een vaste breedte krijgt de `TextBox` die breedte en `TextWrapping.Wrap`, zodat je tijdens het typen dezelfde regelafbreking ziet als in het eindresultaat.
|
||||
- Tijdens het typen blijft de `TextBox` rechthoekig. De staart blijft dan zichtbaar, omdat alleen de tekstlaag verborgen is. Dat is een kleine aanpassing: bij een vorm met staart tekent `Render` de omtrek ook als `IsHidden` aan staat, alleen de tekst niet.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
TextShapeCombo --> ChangeStyle --> StyleSetTextShape["StyleSet.TextShape"]
|
||||
StyleSetTextShape --> BuildShape
|
||||
TailTip --> BuildShape
|
||||
BuildShape --> Render
|
||||
BuildShape --> Bounds
|
||||
HandleDrag["Greep slepen"] --> TailTip
|
||||
```
|
||||
|
||||
## Buiten scope
|
||||
- Een explosie- of uitroepvorm en gebogen staarten. Die zijn later toe te voegen als extra `TextShape`-waarde.
|
||||
- Draaien van het kader (de draaigreep boven het kader in het voorbeeld).
|
||||
@@ -0,0 +1,85 @@
|
||||
---
|
||||
name: Vormenknop
|
||||
overview: Een nieuwe werkbalkknop "Vormen" naast Rechthoek en Ellips, met in de eigenschappenbalk een keuzelijst voor 11 vormen (driehoek, pijl, ster, wolk, ballon, hart, maan, druppel, ei, bliksemschicht, spiraal). Je tekent een vorm door een kader te slepen, net als bij een rechthoek.
|
||||
todos:
|
||||
- id: model
|
||||
content: ToolKind.Shape, ShapeKind-enum, StyleSet.ShapeKind en StyleProps.ShapeKind in StyleSet.cs
|
||||
status: completed
|
||||
- id: geometries
|
||||
content: "ShapeGeometries.cs: de 11 vormen in een eenheidsvierkant, plus IsOpen en Icon; ShapeIconConverter"
|
||||
status: completed
|
||||
- id: annotation
|
||||
content: "FigureAnnotation.cs: Render met transform, HitTest (open/gesloten), spiegelen"
|
||||
status: completed
|
||||
- id: ui
|
||||
content: Werkbalkknop, ShapeKindTemplate, ShapeGroup/ShapeKindCombo in EditorWindow.xaml
|
||||
status: completed
|
||||
- id: wiring
|
||||
content: "EditorWindow.xaml.cs: CreateAnnotation, UpdatePropsPanel, ShapeKind_Changed, knopicoon, DefaultSplitOrder, sneltoets F, hinttekst"
|
||||
status: completed
|
||||
- id: verify
|
||||
content: "Bouwen en handmatig testen: alle vormen tekenen, wisselen, Shift, grepen, spiegelen, kopiëren/plakken, export"
|
||||
status: completed
|
||||
isProject: false
|
||||
---
|
||||
|
||||
# Vormenknop met 11 vormen
|
||||
|
||||
## Gedrag
|
||||
- Er komt een nieuwe gereedschapsknop **Vormen** direct na Ellips. Rechthoek en Ellips blijven zoals ze zijn.
|
||||
- In de eigenschappenbalk staat een keuzelijst **Vorm** (icoon plus naam): Driehoek, Pijl, Ster, Wolk, Ballon, Hart, Maan, Druppel, Ei, Bliksemschicht, Spiraal.
|
||||
- Het icoon op de werkbalkknop toont de laatst gekozen vorm. De keuze wordt per gereedschap onthouden, net als de andere stijlinstellingen.
|
||||
- Tekenen gaat door een kader te slepen; de vorm vult dat kader. Met Shift wordt het kader vierkant. Er zijn 8 formaatgrepen, net als bij een rechthoek.
|
||||
- Eigenschappen: randkleur, vulkleur, dikte, stippellijn en schaduw. De spiraal is een open lijn en krijgt geen vulling.
|
||||
- Bij een geselecteerde vorm kun je via de keuzelijst een andere vorm kiezen (met undo).
|
||||
- Spiegelen (horizontaal en verticaal) werkt voor vormen, zodat de pijl, maan, bliksem, ballon en spiraal ook de andere kant op kunnen.
|
||||
- Sneltoets **F** (figuur), want de meeste voor de hand liggende letters zijn al bezet.
|
||||
- Aanname: **Ballon** is hier een tekstballon zonder tekst (afgeronde rechthoek met een staart linksonder) en **Wolk** een gedachtewolk zonder staart, als losse vorm. Voor een ballon of wolk met tekst blijft het tekstgereedschap bestaan.
|
||||
|
||||
## Technische aanpak
|
||||
|
||||
**1. Model** in [src/Kniip/Editor/StyleSet.cs](src/Kniip/Editor/StyleSet.cs)
|
||||
- `ToolKind.Shape` achteraan toevoegen. Instellingen worden als tekst opgeslagen (`JsonStringEnumConverter`), dus de volgorde breekt niets.
|
||||
- Nieuwe enum `ShapeKind { Triangle, BlockArrow, Star, Cloud, Balloon, Heart, Moon, Drop, Egg, Lightning, Spiral }`.
|
||||
- `StyleSet.ShapeKind` (standaard `Triangle`) en vlag `StyleProps.ShapeKind`.
|
||||
- `PropsFor(ToolKind.Shape)` = Stroke, Fill, Thickness, Dash, Shadow, ShapeKind. `DefaultFor` gebruikt de gewone standaard (rode rand, geen vulling).
|
||||
|
||||
**2. Geometrie** in een nieuw bestand `src/Kniip/Editor/Annotations/ShapeGeometries.cs`
|
||||
- `Geometry For(ShapeKind kind)` bouwt elke vorm in een eenheidsvierkant (0..1), gecachet en bevroren, zoals [CursorGeometries.cs](src/Kniip/Editor/Annotations/CursorGeometries.cs):
|
||||
- Driehoek, Pijl (blokpijl naar rechts), Ster (5 punten), Bliksemschicht: rechte polygonen.
|
||||
- Hart, Druppel, Ei, Maan, Ballon: Bezier- en boogsegmenten in een `StreamGeometry`. De maan wordt een cirkel min een verschoven cirkel (`GeometryCombineMode.Exclude`).
|
||||
- Wolk: een vereniging van cirkels, zoals `TextAnnotation` dat al doet.
|
||||
- Spiraal: een open polyline langs een Archimedes-spiraal (ongeveer 3 omwentelingen).
|
||||
- `bool IsOpen(ShapeKind)` geeft true voor Spiraal.
|
||||
- `Geometry Icon(ShapeKind)` schaalt dezelfde vorm naar 20x20 voor de knop en de keuzelijst. Er komt een `ShapeIconConverter` naar het voorbeeld van [CursorIconConverter.cs](src/Kniip/Editor/CursorIconConverter.cs).
|
||||
|
||||
**3. Annotatie**: nieuw `src/Kniip/Editor/Annotations/FigureAnnotation.cs`, afgeleid van [RectAnnotation.cs](src/Kniip/Editor/Annotations/RectAnnotation.cs), zodat grepen, verplaatsen en Shift al werken.
|
||||
- `Kind => ToolKind.Shape`. De vorm komt uit `Style.ShapeKind`, zodat wisselen via `ChangeStyle` meteen werkt.
|
||||
- `Render`: de eenheidsgeometrie klonen met een `MatrixTransform` naar `Rect` (plus een spiegeling met `FlipX`/`FlipY`). De lijndikte schaalt daardoor niet mee. Tekenen gebeurt via `RenderHelpers.DrawWithShadow`, zonder vulling bij een open vorm.
|
||||
- `HitTest`: `FillContains` voor gesloten vormen, `StrokeContains` met tolerantie voor de spiraal.
|
||||
- `CanFlip => true`; `Flip` wisselt `FlipX` of `FlipY`.
|
||||
|
||||
**4. Editor** in [src/Kniip/Editor/EditorWindow.xaml](src/Kniip/Editor/EditorWindow.xaml) en [EditorWindow.xaml.cs](src/Kniip/Editor/EditorWindow.xaml.cs)
|
||||
- XAML: een `RadioButton Tag="Shape" ItemId="Shape" Title="Vormen"` na Ellips, met een `Path x:Name="ShapeToolIcon"`. Daarnaast een `ShapeKindTemplate` (icoon via de converter, plus een label per waarde, net als `CursorKindTemplate`) en een `ShapeGroup` met `ShapeKindCombo` in de eigenschappenbalk.
|
||||
- Code:
|
||||
- `ShapeKindValues` als `ItemsSource`.
|
||||
- `"Shape"` na `"Ellipse"` in `DefaultSplitOrder`. `MergeToolbarOrder` zet de nieuwe knop bij bestaande gebruikers automatisch na Ellips.
|
||||
- `CreateAnnotation`: `ToolKind.Shape => new FigureAnnotation { Rect = new Rect(p, p), Style = style }`.
|
||||
- `UpdatePropsPanel`: zichtbaarheid van `ShapeGroup` en selectie van `ShapeKindCombo`. De vulling wordt verborgen bij Spiraal.
|
||||
- Handler `ShapeKind_Changed` roept `ChangeStyle("shape", s => s.ShapeKind = kind)` aan en werkt `ShapeToolIcon` bij. Bij het starten wordt het icoon ook gezet.
|
||||
- Sneltoets `Key.F` en een hinttekst: "Sleep om de gekozen vorm te tekenen. Shift = gelijke breedte en hoogte. Kies een andere vorm in de balk eronder."
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
ShapeKindCombo --> ChangeStyle --> StyleShapeKind["StyleSet.ShapeKind"]
|
||||
StyleShapeKind --> ShapeGeometries
|
||||
ShapeGeometries --> FigureAnnotation
|
||||
ShapeGeometries --> ShapeIconConverter
|
||||
ShapeIconConverter --> ShapeToolIcon
|
||||
ShapeIconConverter --> ShapeKindTemplate
|
||||
```
|
||||
|
||||
## Buiten scope
|
||||
- Draaien van vormen. Een pijl omhoog of omlaag kan daardoor niet; links en rechts wel, via spiegelen.
|
||||
- Tekst in een vorm. Daarvoor blijft het tekstgereedschap met ballon en wolk.
|
||||
- Instelbare details per vorm, zoals het aantal punten van de ster, de dikte van de maan of het aantal windingen van de spiraal.
|
||||
@@ -2,6 +2,85 @@
|
||||
|
||||
Alle noemenswaardige wijzigingen in kniip staan in dit bestand. De nieuwste versie staat bovenaan.
|
||||
|
||||
## 2.1.0 - 2026-11-11
|
||||
|
||||
### Nieuw
|
||||
|
||||
- **Printen.**\
|
||||
Druk een knipsel met je aantekeningen af. De knop met het printericoon staat links van het tandwiel, of druk op Ctrl+P. Je kiest de printer en de afdrukstand in het printvenster van Windows. Het knipsel komt in het midden van de pagina; is het te groot, dan wordt het verkleind.
|
||||
- **Mappen openen.**\
|
||||
Links van de printknop staat een knop met een mapicoon. Klik erop en kies **Open Screenshot map** of **Open Screencast map**. Je krijgt dezelfde mappen als in het systeemvakmenu: de opslagmap uit de instellingen, anders de map waarin je het laatst hebt opgeslagen.
|
||||
- Staan de linialen aan, dan zit er ook een mapknop in het hoekje waar de linialen samenkomen. Die opent hetzelfde menu, met **Screenshotmap openen** en **Screencastmap openen**. Rechtsklik op het hoekje toont nog steeds het menu om hulplijnen te wissen of de linialen te verbergen.
|
||||
- Is er nog geen screencastmap, dan maakt kniip die aan (`Video's\kniip`). Daardoor kun je **Screencasts** in het systeemvakmenu nu ook kiezen voordat je je eerste screencast hebt opgeslagen.
|
||||
- **Meer opslagopties onder de Opslaan-knop.**\
|
||||
Rechts in de Opslaan-knop staat een klein pijltje. Klik erop voor een menu met de andere manier van opslaan en **Opslaan als...**. Staat de standaardknop in de instellingen op **Opslaan en kopiëren**, dan kies je hier **Opslaan** zonder te kopiëren; staat hij op **Opslaan**, dan kies je hier **Opslaan en kopiëren**. **Opslaan als...** vraagt altijd waar het bestand moet komen. Het menu staat ook in het screencastvenster, met het gekozen formaat erbij, zoals **Opslaan als MP4**.
|
||||
|
||||
## 2.0.0 - 2026-11-10
|
||||
|
||||
### Nieuw
|
||||
|
||||
- **OCR (tekst herkennen).**\
|
||||
Lees tekst van het scherm en kopieer hem. Naast de knoppen voor een schermafdruk en een screencast staat nu de knop **OCR**, met hetzelfde icoon als in het Knipprogramma. Klik erop, sleep een rechthoek, en de tekst daarin opent in een apart venster. Daar pas je hem aan en kopieer je alles, of alleen wat je selecteert. De editor gaat tijdens het kiezen even opzij en komt daarna terug.
|
||||
- Druk in de editor op Ctrl+Shift+T om de tekst van het geopende knipsel te lezen, met je aantekeningen erbij.
|
||||
- Je vindt OCR ook in het menu van het icoon in het systeemvak, onder **OCR (tekst herkennen)...**. Geef het in de instellingen onder **Sneltoetsen** een eigen sneltoets.
|
||||
- kniip gebruikt de tekstherkenning van Windows, in de talen van je Windows-account. Vindt Windows geen taal voor tekstherkenning, dan staat in het venster hoe je er een toevoegt.
|
||||
- **Verloop in elke richting.**\
|
||||
De richting van een verloop als achtergrond kies je nu met een draaiknop in plaats van uit vier vaste richtingen. Klik op de knop naast de kleuren en sleep de knop rond de cirkel. Je ziet het verloop meteen meedraaien.
|
||||
- Hij klikt vast op de acht hoofdrichtingen. Houd Shift ingedrukt voor stappen van 15°, of Alt om niet vast te klikken. Je kunt ook de hoek in graden typen, of scrollen boven de knop.
|
||||
- **Vanuit het midden** staat nu als knop onder de cirkel.
|
||||
- Elke nieuwe richting is één stap in **Ongedaan maken**, ook als je lang sleept.
|
||||
|
||||
### Gewijzigd
|
||||
|
||||
- Het blokjespatroon achter transparante delen is lichter en daardoor rustiger om naar te kijken.
|
||||
|
||||
## 1.1.9 - 2026-11-10
|
||||
|
||||
### Gewijzigd
|
||||
|
||||
- Klik je op het icoon in het systeemvak, dan maak je direct een schermafdruk: sleep een rechthoek om te knippen. Het menu opent nog steeds met de rechtermuisknop. Daarin staat **Rechthoek knippen** nu vetgedrukt als standaardactie, en **Bewerken vanaf klembord** staat er nog gewoon in.
|
||||
|
||||
## 1.1.8 - 2026-11-10
|
||||
|
||||
### Nieuw
|
||||
|
||||
- **Transparant canvas en achtergrond.**\
|
||||
Een leeg canvas is nu transparant in plaats van wit. In de editor zie je dat aan een blokjespatroon, en een opgeslagen PNG houdt de transparantie. Wil je toch een achtergrond, kies dan het selectiegereedschap zonder iets te selecteren: in de balk met eigenschappen staat **Achtergrond**. Kies **Geen (transparant)**, **Kleur**, **Verloop** (twee kleuren en een richting) of **Afbeelding** (vult het hele canvas).
|
||||
- De achtergrond komt alleen onder de transparante delen. Bij een gewoon schermknipsel staat de keuze er daarom niet. Bij een geopende PNG met transparantie wel.
|
||||
- Vervagen en pixeleren werken ook op de achtergrond. **Samenvoegen** laat de achtergrond apart, zodat je hem daarna nog kunt wijzigen.
|
||||
- Een JPEG kent geen transparantie. Daarin, en in de bitmap op het klembord voor programma's die geen PNG lezen, worden transparante delen wit.
|
||||
- Sommige programma's zetten een afbeelding op het klembord die volledig doorzichtig lijkt. Zo'n afbeelding opent kniip nu als gewone, ondoorzichtige afbeelding, zodat hij niet onzichtbaar wordt.
|
||||
- Elke wijziging van de achtergrond is één stap in **Ongedaan maken**.
|
||||
- **Drie-split-weergave.**\
|
||||
Een derde indeling voor de editor: de gereedschappen staan boven, **Opslaan**, **Uploaden**, **Kopiëren** en de zoomknoppen onder, en **Nieuw** en **Screencast** apart. Kies de indeling in de instellingen onder **Weergave** > **Werkbalk**, of klik met rechts op een werkbalk en kies **Indeling**.
|
||||
- De balk met **Nieuw** en **Screencast** staat links, rechts, boven of onder het canvas, of **Zwevend** boven het canvas. Kies de plek in de instellingen of via rechtsklik > **Nieuw en Screencast**.
|
||||
- Zwevende knoppen versleep je aan het greepje of de rand naar elke plek boven het canvas. Bij de boven- of onderrand klikken ze vast en liggen ze horizontaal, verder liggen ze verticaal. De plek wordt onthouden. Klik met rechts en kies **Zwevende knoppen vastzetten** om ze niet per ongeluk te verslepen.
|
||||
- In de balk staan alleen de iconen. Ctrl+H verbergt ook deze balk, en **Standaard herstellen** zet hem weer links.
|
||||
- **Linialen en hulplijnen.**\
|
||||
Boven en links van het canvas staan linialen met een schaalverdeling in pixels van de afbeelding. Ze zoomen en scrollen mee, en een blauw streepje laat zien waar de muis staat. Zet ze aan of uit met de knop **Linialen** naast de zoomknoppen, of met Ctrl+R.
|
||||
- Sleep vanuit een liniaal naar het canvas voor een hulplijn: de bovenste liniaal geeft een horizontale lijn, de linker een verticale. Met het selectiegereedschap versleep je een hulplijn. Sleep hem terug naar de liniaal of buiten de afbeelding om hem te verwijderen. Klik met rechts op het hoekje tussen de linialen of op het canvas en kies **Hulplijnen wissen** om ze allemaal weg te halen.
|
||||
- Objecten klikken vast aan hulplijnen en aan de randen en het midden van de afbeelding: bij verplaatsen met de linker-, midden- of rechterkant (en boven, midden of onder), bij tekenen, bij het slepen van grepen en bij bijsnijden. De lijn waar het object aan vastklikt, licht roze op. Houd Alt ingedrukt om niet vast te klikken.
|
||||
- Hulplijnen zijn alleen een hulpmiddel in de editor. Ze komen niet in het opgeslagen of gekopieerde knipsel.
|
||||
- **Uitlijnen, spiegelen en volgorde.**\
|
||||
Zodra je een object selecteert, staat in de balk met eigenschappen de knop **Uitlijnen**. Klik erop en er klappen knoppen uit om het object links, in het midden of rechts, en boven, in het midden of onder op de afbeelding te zetten. Ook kun je het horizontaal of verticaal spiegelen, en naar voren of naar achteren brengen. Klik nog eens op **Uitlijnen** om de knoppen weer in te klappen. Ze klappen ook in als je niets meer selecteert. Dezelfde opties staan in het rechtsklikmenu van een object, onder **Uitlijnen op de afbeelding**. Daar staat ook **Midden van de afbeelding**.
|
||||
- Spiegelen werkt voor lijnen, pijlen, markeringen uit de vrije hand, cursors, emoji en vormen. Bij rechthoeken, ellipsen, tekst, stappen en vervagen is de knop uitgeschakeld.
|
||||
- Elke klik is één stap in **Ongedaan maken**.
|
||||
- **Vormen.**\
|
||||
Na **Ellips** staat een nieuwe knop **Vormen** (sneltoets F). Kies in de balk met eigenschappen welke vorm je tekent: driehoek, pijl, ster, wolk, ballon, hart, maan, druppel, ei, bliksemschicht of spiraal. Het icoon op de knop laat de gekozen vorm zien.
|
||||
- Sleep een kader om de vorm te tekenen. Met Shift worden breedte en hoogte gelijk. Met de grepen pas je het formaat later aan.
|
||||
- Je kiest randkleur, vulkleur, dikte, stippellijn en schaduw, net als bij een rechthoek. De spiraal is een open lijn en heeft geen vulling.
|
||||
- Selecteer een vorm en kies een andere in de keuzelijst om hem te vervangen. Spiegel of draai een pijl, maan of bliksem om hem een andere kant op te laten wijzen.
|
||||
- **Draaien van vormen en emoji.**\
|
||||
Boven een geselecteerde vorm of emoji staat een ronde greep. Sleep die rond om het object te draaien. Het draait om zijn midden en klikt vast op 0°, 90°, 180° en 270°. Houd Shift ingedrukt voor stappen van 15°, of Alt om niet vast te klikken. Tijdens het draaien staat de hoek onderin de statusbalk.
|
||||
- Onder **Uitlijnen** staan knoppen om 90° linksom of rechtsom te draaien. Die opties staan ook in het rechtsklikmenu, samen met **Draaiing herstellen**. Dubbelklik op de ronde greep om de draaiing ook terug te zetten.
|
||||
- Een gedraaide vorm maak je met de grepen gewoon groter of kleiner. De schaduw valt altijd rechtsonder.
|
||||
- Elke draaiing is één stap in **Ongedaan maken**.
|
||||
- **Tekstballon en -wolk.**\
|
||||
Bij **Tekst in kader** kies je in de balk met eigenschappen de vorm: **Kader**, **Ballon**, **Ovale ballon** of **Wolk**. Ballon en wolk hebben een staart waarmee je iets aanwijst.
|
||||
- Sleep de punt van de staart naar wat je wilt aanwijzen. Verplaats je het hele object, dan schuift de punt mee.
|
||||
- Bij een wolk bestaat de staart uit steeds kleinere bolletjes. Kies het aantal bolletjes, of laat het op **Auto** staan.
|
||||
- Een geselecteerd tekstobject heeft formaatgrepen. Maak het kader breder of smaller, dan loopt de tekst door naar de volgende regel. Dubbelklik op een greep om het kader weer automatisch op de tekst te laten passen.
|
||||
|
||||
## 1.1.7 - 2026-11-09
|
||||
|
||||
### Gewijzigd
|
||||
|
||||
@@ -0,0 +1,674 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
the GNU General Public License is intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users. We, the Free Software Foundation, use the
|
||||
GNU General Public License for most of our software; it applies also to
|
||||
any other work released this way by its authors. You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.
|
||||
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.
|
||||
|
||||
For the developers' and authors' protection, the GPL clearly explains
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.
|
||||
|
||||
Some devices are designed to deny users access to install or run
|
||||
modified versions of the software inside them, although the manufacturer
|
||||
can do so. This is fundamentally incompatible with the aim of
|
||||
protecting users' freedom to change the software. The systematic
|
||||
pattern of such abuse occurs in the area of products for individuals to
|
||||
use, which is precisely where it is most unacceptable. Therefore, we
|
||||
have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we
|
||||
stand ready to extend this provision to those domains in future versions
|
||||
of the GPL, as needed to protect the freedom of users.
|
||||
|
||||
Finally, every program is threatened constantly by software patents.
|
||||
States should not allow patents to restrict development and use of
|
||||
software on general-purpose computers, but in those that do, we wish to
|
||||
avoid the special danger that patents applied to a free program could
|
||||
make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Use with the GNU Affero General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU Affero General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, your program's commands
|
||||
might be different; for a GUI interface, you would use an "about box".
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU GPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
|
||||
The GNU General Public License does not permit incorporating your program
|
||||
into proprietary programs. If your program is a subroutine library, you
|
||||
may consider it more useful to permit linking proprietary applications with
|
||||
the library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License. But first, please read
|
||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
||||
@@ -25,7 +25,7 @@ kniip is een annotatie-editor die naast het Windows Knipprogramma werkt. Maak ee
|
||||
- **Knippen tot op de pixel.** Rechthoek of venster, met een vergrootglas voor precieze hoeken en een instelbare vertraging om eerst een menu of tooltip te openen.
|
||||
- **Aantekenen.** Pijlen en lijnen die je kunt buigen, tekst, vormen, genummerde stappen, markeerstift, emoji en vervaging.
|
||||
- **Screencasts.** Neem een deel van je scherm op en exporteer als video of GIF.
|
||||
- **Delen.** Opslaan, kopiëren naar het klembord of uploaden en de link meteen delen.
|
||||
- **Delen.** Opslaan, kopiëren naar het klembord, printen of uploaden en de link meteen delen.
|
||||
- **Geschiedenis.** Vind eerdere knipsels en uploads terug, met miniaturen en zoeken.
|
||||
- **Jouw werkbalk.** Kies een of twee werkbalken, versleep knoppen en verberg wat je niet gebruikt.
|
||||
- **Altijd bij.** kniip meldt zelf wanneer er een nieuwe versie is en werkt zichzelf bij met een klik.
|
||||
@@ -34,7 +34,7 @@ kniip is een annotatie-editor die naast het Windows Knipprogramma werkt. Maak ee
|
||||
|
||||
| Versie | Installer |
|
||||
| ------ | --------- |
|
||||
| 1.1.7 | [Kniip-Setup-1.1.7.exe](https://git.hibbit.net/rogier/kniip/releases/download/1.1.7/Kniip-Setup-1.1.7.exe) |
|
||||
| 2.1.0 | [Kniip-Setup-2.1.0.exe](https://git.hibbit.net/rogier/kniip/releases/download/2.1.0/Kniip-Setup-2.1.0.exe) |
|
||||
|
||||
Oudere versies vind je bij [alle releases](https://git.hibbit.net/rogier/kniip/releases).
|
||||
|
||||
@@ -80,7 +80,7 @@ Vereisten:
|
||||
Bouwen:
|
||||
|
||||
```powershell
|
||||
.\build.ps1 # versie 1.1.7
|
||||
.\build.ps1 # versie 2.1.0
|
||||
.\build.ps1 -Version 1.2.0 # andere versie
|
||||
```
|
||||
|
||||
@@ -117,8 +117,10 @@ Onder **Instellingen > Update** controleert kniip via de API van git.hibbit.net
|
||||
|
||||
kniip controleert ook zelf bij het starten en daarna elke 24 uur. Is er een nieuwere versie, dan staat er een blauwe stip op de knop **Instellingen** in de editor en op de tab **Update**. Dit zet je uit met **Automatisch controleren op nieuwe versies** in die tab.
|
||||
|
||||
Voorwaarden:
|
||||
## Licentie
|
||||
|
||||
- De repository `rogier/kniip` is publiek; kniip gebruikt geen token.
|
||||
- De tag is een versienummer (`v1.2.0`); concepten en voorversies worden overgeslagen.
|
||||
- De bijlage heet `Kniip-Setup-<versie>.exe`. Zonder bijlage toont kniip alleen de changelog en een link naar de releasepagina.
|
||||
Copyright © 2026 Rogier Meijer
|
||||
|
||||
kniip is vrije software: je mag het verspreiden en aanpassen onder de voorwaarden van de [GNU General Public License](LICENSE), versie 3 of (naar keuze) een latere versie, zoals gepubliceerd door de Free Software Foundation. kniip wordt verspreid zonder enige garantie; zie de [licentie](LICENSE) voor de details.
|
||||
|
||||
kniip gebruikt [Markdig](https://github.com/xoofx/markdig) (BSD-2-Clause), [ScreenRecorderLib](https://github.com/sskodje/ScreenRecorderLib) (MIT) en [Twemoji](https://github.com/jdecked/twemoji) (CC-BY 4.0). Zie [`NOTICE.txt`](src/Kniip/Assets/Emoji/NOTICE.txt) voor de herkomst van de emoji.
|
||||
@@ -12,7 +12,7 @@
|
||||
# .\build.ps1 -SkipTest -> zonder testexe
|
||||
|
||||
param(
|
||||
[string]$Version = "1.1.7",
|
||||
[string]$Version = "2.1.0",
|
||||
[string]$Runtime = "win-x64",
|
||||
[switch]$SkipTest
|
||||
)
|
||||
|
||||
+3
-1
@@ -5,7 +5,7 @@
|
||||
#define AppName "kniip"
|
||||
#define AppExe "Kniip.exe"
|
||||
#ifndef AppVersion
|
||||
#define AppVersion "1.1.7"
|
||||
#define AppVersion "2.1.0"
|
||||
#endif
|
||||
#ifndef PublishDir
|
||||
#define PublishDir "..\artifacts\publish"
|
||||
@@ -47,6 +47,8 @@ Type: files; Name: "{autodesktop}\Knip+.lnk"
|
||||
|
||||
[Files]
|
||||
Source: "{#PublishDir}\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
|
||||
Source: "..\LICENSE"; DestDir: "{app}"; DestName: "LICENSE.txt"; Flags: ignoreversion
|
||||
Source: "..\src\Kniip\Assets\Emoji\NOTICE.txt"; DestDir: "{app}"; DestName: "NOTICE-emoji.txt"; Flags: ignoreversion
|
||||
|
||||
[Icons]
|
||||
Name: "{autoprograms}\{#AppName}"; Filename: "{app}\{#AppExe}"
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 488 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 955 KiB |
+74
-19
@@ -21,6 +21,9 @@ namespace Kniip;
|
||||
|
||||
public partial class App : Application
|
||||
{
|
||||
/// <summary>Waar een rechthoekknipsel heen gaat: naar de editor of naar tekstherkenning.</summary>
|
||||
private enum SnipTarget { Editor, Ocr }
|
||||
|
||||
private SingleInstance? _singleInstance;
|
||||
private TrayIcon? _tray;
|
||||
private SettingsWindow? _settingsWindow;
|
||||
@@ -28,6 +31,8 @@ public partial class App : Application
|
||||
private readonly List<RegionCaptureWindow> _snipWindows = new();
|
||||
private List<SnappedWindow> _snipSnapshot = new();
|
||||
private CountdownWindow? _snipCountdown;
|
||||
private SnipTarget _snipTarget;
|
||||
private Action? _snipFinished;
|
||||
private uint? _ignoredClipboardSequence;
|
||||
|
||||
public static App Instance => (App)Current;
|
||||
@@ -94,6 +99,7 @@ public partial class App : Application
|
||||
_tray.OpenFileRequested += OpenFileWithDialog;
|
||||
_tray.SnipRequested += BeginRegionSnip;
|
||||
_tray.WindowSnipRequested += BeginWindowSnip;
|
||||
_tray.TextRecognitionRequested += () => BeginTextSnip();
|
||||
_tray.SetCaptureDelay(Settings.CaptureDelaySeconds);
|
||||
_tray.CaptureDelayChanged += seconds =>
|
||||
{
|
||||
@@ -205,6 +211,11 @@ public partial class App : Application
|
||||
timer.Tick += (_, _) =>
|
||||
{
|
||||
timer.Stop();
|
||||
if (_snipCountdown == null && _snipWindows.Count == 0)
|
||||
{
|
||||
_snipTarget = SnipTarget.Editor;
|
||||
}
|
||||
|
||||
BeginSnip(pickWindow: false);
|
||||
};
|
||||
timer.Start();
|
||||
@@ -216,8 +227,34 @@ public partial class App : Application
|
||||
/// <summary>Bevriest alle schermen (na de ingestelde vertraging) en laat een venster aanklikken; het resultaat opent in de editor.</summary>
|
||||
public void BeginWindowSnip() => BeginDelayedSnip(pickWindow: true);
|
||||
|
||||
private void BeginDelayedSnip(bool pickWindow)
|
||||
/// <summary>Als een rechthoekknipsel, maar de herkende tekst opent in een apart venster in plaats van de editor.</summary>
|
||||
/// <param name="finished">Na het kiezen of annuleren, vóórdat het tekstvenster opent; bijvoorbeeld om de editor terug te zetten.</param>
|
||||
public void BeginTextSnip(Action? finished = null)
|
||||
{
|
||||
if (_snipCountdown != null || _snipWindows.Count > 0 || Recorder.Stage == RecordingStage.Selecting)
|
||||
{
|
||||
finished?.Invoke();
|
||||
return;
|
||||
}
|
||||
|
||||
_snipFinished = finished;
|
||||
BeginDelayedSnip(pickWindow: false, SnipTarget.Ocr);
|
||||
}
|
||||
|
||||
private void RunSnipFinished()
|
||||
{
|
||||
Action? finished = _snipFinished;
|
||||
_snipFinished = null;
|
||||
finished?.Invoke();
|
||||
}
|
||||
|
||||
private void BeginDelayedSnip(bool pickWindow, SnipTarget target = SnipTarget.Editor)
|
||||
{
|
||||
if (_snipCountdown == null && _snipWindows.Count == 0)
|
||||
{
|
||||
_snipTarget = target;
|
||||
}
|
||||
|
||||
if (_snipCountdown != null)
|
||||
{
|
||||
return;
|
||||
@@ -243,7 +280,11 @@ public partial class App : Application
|
||||
};
|
||||
timer.Start();
|
||||
};
|
||||
countdown.Cancelled += EndCountdown;
|
||||
countdown.Cancelled += () =>
|
||||
{
|
||||
EndCountdown();
|
||||
RunSnipFinished();
|
||||
};
|
||||
_snipCountdown = countdown;
|
||||
ApplyHotkeys();
|
||||
countdown.Start();
|
||||
@@ -326,7 +367,14 @@ public partial class App : Application
|
||||
_snipWindows.Clear();
|
||||
List<SnappedWindow> snapshot = _snipSnapshot;
|
||||
_snipSnapshot = new();
|
||||
if (image != null)
|
||||
SnipTarget target = _snipTarget;
|
||||
_snipTarget = SnipTarget.Editor;
|
||||
RunSnipFinished();
|
||||
if (image != null && target == SnipTarget.Ocr)
|
||||
{
|
||||
BringToFront(OcrResultWindow.Open(image));
|
||||
}
|
||||
else if (image != null)
|
||||
{
|
||||
string? windowName = area is { } a ? PhysicalWindow.ProgramNameAt(snapshot, a) : null;
|
||||
OpenEditor(image, windowName);
|
||||
@@ -356,6 +404,7 @@ public partial class App : Application
|
||||
Bind(Settings.RecordHotkey, () => Recorder.Toggle(ScreencastFormat.Mp4));
|
||||
Bind(Settings.GifHotkey, () => Recorder.Toggle(ScreencastFormat.Gif));
|
||||
Bind(Settings.ClipboardHotkey, OpenFromClipboard);
|
||||
Bind(Settings.OcrHotkey, () => BeginTextSnip());
|
||||
if (_snipCountdown is { } countdown)
|
||||
{
|
||||
// Alleen tijdens het aftellen; daarna moet Esc gewoon naar het actieve programma.
|
||||
@@ -366,7 +415,8 @@ public partial class App : Application
|
||||
}
|
||||
|
||||
_tray?.SetHotkeys(HotkeyDisplay(Settings.SnipHotkey), HotkeyDisplay(Settings.WindowHotkey),
|
||||
HotkeyDisplay(Settings.RecordHotkey), HotkeyDisplay(Settings.GifHotkey), HotkeyDisplay(Settings.ClipboardHotkey));
|
||||
HotkeyDisplay(Settings.RecordHotkey), HotkeyDisplay(Settings.GifHotkey), HotkeyDisplay(Settings.ClipboardHotkey),
|
||||
HotkeyDisplay(Settings.OcrHotkey));
|
||||
}
|
||||
|
||||
/// <summary>Start een screencast: eerst het gebied kiezen, daarna de knoppenbalk.</summary>
|
||||
@@ -427,11 +477,11 @@ public partial class App : Application
|
||||
OpenEditor(image);
|
||||
}
|
||||
|
||||
/// <summary>Editor met een leeg wit canvas, voor als er (nog) geen knipsel is.</summary>
|
||||
/// <summary>Editor met een leeg, transparant canvas, voor als er (nog) geen knipsel is.</summary>
|
||||
public void OpenBlankEditor(Window? placeLike = null)
|
||||
{
|
||||
const int width = 1280, height = 720;
|
||||
byte[] pixels = Enumerable.Repeat((byte)255, width * height * 4).ToArray();
|
||||
var pixels = new byte[width * height * 4];
|
||||
BitmapSource blank = BitmapSource.Create(width, height, 96, 96, System.Windows.Media.PixelFormats.Bgra32, null, pixels, width * 4);
|
||||
var editor = new EditorWindow(blank, Settings, isBlank: true);
|
||||
if (placeLike != null)
|
||||
@@ -512,20 +562,25 @@ public partial class App : Application
|
||||
BringToFront(_historyWindow);
|
||||
}
|
||||
|
||||
private IReadOnlyList<(string Label, string? Path)> TrayFolders()
|
||||
{
|
||||
static string? Existing(params string?[] folders) =>
|
||||
folders.FirstOrDefault(folder => folder is { Length: > 0 } && Directory.Exists(folder));
|
||||
/// <summary>Map met knipsels: de opslagmap, anders de laatst gebruikte map, anders Afbeeldingen.</summary>
|
||||
public string SnipFolder =>
|
||||
ExistingFolder(Settings.SnipSaveFolder, Settings.LastSaveFolder)
|
||||
?? Environment.GetFolderPath(Environment.SpecialFolder.MyPictures);
|
||||
|
||||
return new (string, string?)[]
|
||||
{
|
||||
("Schermafbeeldingen", Existing(CaptureCoordinator.ScreenshotsFolder)),
|
||||
("Knipsels", Existing(Settings.SnipSaveFolder, Settings.LastSaveFolder,
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.MyPictures))),
|
||||
("Screencasts", Existing(Settings.VideoSaveFolder, Settings.LastVideoFolder,
|
||||
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyVideos), "kniip")))
|
||||
};
|
||||
}
|
||||
/// <summary>Map met screencasts: de opslagmap, anders de laatst gebruikte map, anders Video's\kniip (bestaat mogelijk nog niet).</summary>
|
||||
public string VideoFolder =>
|
||||
ExistingFolder(Settings.VideoSaveFolder, Settings.LastVideoFolder)
|
||||
?? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyVideos), "kniip");
|
||||
|
||||
private static string? ExistingFolder(params string?[] folders) =>
|
||||
folders.FirstOrDefault(folder => folder is { Length: > 0 } && Directory.Exists(folder));
|
||||
|
||||
private IReadOnlyList<(string Label, string? Path)> TrayFolders() => new (string, string?)[]
|
||||
{
|
||||
("Schermafbeeldingen", ExistingFolder(CaptureCoordinator.ScreenshotsFolder)),
|
||||
("Knipsels", SnipFolder),
|
||||
("Screencasts", VideoFolder)
|
||||
};
|
||||
|
||||
public void ShowBalloon(string title, string text) => _tray?.ShowBalloon(title, text);
|
||||
|
||||
|
||||
@@ -170,7 +170,8 @@ internal static class ClipboardImageReader
|
||||
|
||||
try
|
||||
{
|
||||
return PixelUtil.Decode(new MemoryStream(file));
|
||||
BitmapSource image = PixelUtil.Decode(new MemoryStream(file));
|
||||
return PixelUtil.IsFullyTransparent(image) ? OpaqueCopy(image) : image;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
|
||||
@@ -14,6 +14,12 @@ public sealed class AnnotationSurface : FrameworkElement
|
||||
private static readonly Brush AccentBrush = RenderHelpers.Freeze(new SolidColorBrush(Palette.Accent));
|
||||
private static readonly Brush GhostHandleBrush = RenderHelpers.Freeze(new SolidColorBrush(Color.FromArgb(170, 255, 255, 255)));
|
||||
private static readonly Brush CropShadeBrush = RenderHelpers.Freeze(new SolidColorBrush(Color.FromArgb(120, 0, 0, 0)));
|
||||
private static readonly Brush CheckerLight = RenderHelpers.Freeze(new SolidColorBrush(Colors.White));
|
||||
private static readonly Brush CheckerDark = RenderHelpers.Freeze(new SolidColorBrush(Color.FromRgb(0xEF, 0xEF, 0xEF)));
|
||||
|
||||
public static readonly Color GuideColor = Color.FromRgb(0, 180, 255);
|
||||
private static readonly Brush GuideBrush = RenderHelpers.Freeze(new SolidColorBrush(GuideColor));
|
||||
private static readonly Brush SnapBrush = RenderHelpers.Freeze(new SolidColorBrush(Color.FromRgb(255, 45, 160)));
|
||||
|
||||
public EditorDocument? Document { get; set; }
|
||||
|
||||
@@ -21,6 +27,19 @@ public sealed class AnnotationSurface : FrameworkElement
|
||||
|
||||
public Rect? CropRect { get; set; }
|
||||
|
||||
/// <summary>Verticale hulplijnen (X in afbeeldingspixels); worden niet geëxporteerd.</summary>
|
||||
public IReadOnlyList<double> GuidesX { get; set; } = System.Array.Empty<double>();
|
||||
|
||||
/// <summary>Horizontale hulplijnen (Y in afbeeldingspixels).</summary>
|
||||
public IReadOnlyList<double> GuidesY { get; set; } = System.Array.Empty<double>();
|
||||
|
||||
public bool ShowGuides { get; set; }
|
||||
|
||||
/// <summary>Lijn waaraan het object tijdens het slepen vastklikt, om die op te laten lichten.</summary>
|
||||
public double? SnapX { get; set; }
|
||||
|
||||
public double? SnapY { get; set; }
|
||||
|
||||
/// <summary>Schaal van de weergave (DIP per afbeeldingspixel), om grepen altijd even groot te tonen.</summary>
|
||||
public double ViewScale { get; set; } = 1;
|
||||
|
||||
@@ -43,6 +62,11 @@ public sealed class AnnotationSurface : FrameworkElement
|
||||
return;
|
||||
}
|
||||
|
||||
if (Document.HasTransparency)
|
||||
{
|
||||
dc.DrawRectangle(CreateChecker(), null, new Rect(0, 0, Document.Width, Document.Height));
|
||||
}
|
||||
|
||||
DocumentRenderer.Render(dc, Document);
|
||||
|
||||
if (Selected != null && !Selected.IsHidden && Document.Items.Contains(Selected))
|
||||
@@ -58,14 +82,105 @@ public sealed class AnnotationSurface : FrameworkElement
|
||||
dc.DrawRectangle(null, CreateDashedPen(Brushes.White), crop);
|
||||
dc.DrawRectangle(null, CreateDashedPen(AccentBrush, 4), crop);
|
||||
}
|
||||
|
||||
if (ShowGuides)
|
||||
{
|
||||
DrawGuides(dc, Document);
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawGuides(DrawingContext dc, EditorDocument document)
|
||||
{
|
||||
var guidePen = new Pen(GuideBrush, 1 / ViewScale);
|
||||
var snapPen = new Pen(SnapBrush, 1 / ViewScale);
|
||||
foreach (double x in GuidesX)
|
||||
{
|
||||
dc.DrawLine(guidePen, new Point(x, 0), new Point(x, document.Height));
|
||||
}
|
||||
|
||||
foreach (double y in GuidesY)
|
||||
{
|
||||
dc.DrawLine(guidePen, new Point(0, y), new Point(document.Width, y));
|
||||
}
|
||||
|
||||
if (SnapX is double sx)
|
||||
{
|
||||
dc.DrawLine(snapPen, new Point(sx, 0), new Point(sx, document.Height));
|
||||
}
|
||||
|
||||
if (SnapY is double sy)
|
||||
{
|
||||
dc.DrawLine(snapPen, new Point(0, sy), new Point(document.Width, sy));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Index van de hulplijn onder het punt; <paramref name="vertical"/> is true voor een verticale lijn (X).</summary>
|
||||
public int HitGuide(Point p, out bool vertical)
|
||||
{
|
||||
double tolerance = 4 / ViewScale;
|
||||
vertical = true;
|
||||
for (int i = GuidesX.Count - 1; i >= 0; i--)
|
||||
{
|
||||
if (System.Math.Abs(GuidesX[i] - p.X) <= tolerance)
|
||||
{
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
vertical = false;
|
||||
for (int i = GuidesY.Count - 1; i >= 0; i--)
|
||||
{
|
||||
if (System.Math.Abs(GuidesY[i] - p.Y) <= tolerance)
|
||||
{
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
/// <summary>Plek van de draaigreep: een vaste schermafstand boven het (gedraaide) kader.</summary>
|
||||
public Point? RotateHandle(Annotation item)
|
||||
{
|
||||
if (!item.CanRotate)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
Rect f = item.Frame;
|
||||
var local = new Point(f.X + f.Width / 2, f.Top - (3 + 22) / ViewScale);
|
||||
return item.RotationMatrix.Transform(local);
|
||||
}
|
||||
|
||||
public bool HitRotateHandle(Annotation item, Point p) =>
|
||||
RotateHandle(item) is Point h && (h - p).Length <= HandleSize / 2 + 3 / ViewScale;
|
||||
|
||||
private void DrawSelection(DrawingContext dc, Annotation item)
|
||||
{
|
||||
Rect bounds = item.Bounds;
|
||||
double pad = 3 / ViewScale;
|
||||
bounds.Inflate(pad, pad);
|
||||
dc.DrawRectangle(null, CreateDashedPen(AccentBrush), bounds);
|
||||
double size = HandleSize;
|
||||
var pen = new Pen(AccentBrush, 1.2 / ViewScale);
|
||||
|
||||
if (item.CanRotate)
|
||||
{
|
||||
Rect frame = item.Frame;
|
||||
frame.Inflate(pad, pad);
|
||||
dc.PushTransform(new MatrixTransform(item.RotationMatrix));
|
||||
dc.DrawRectangle(null, CreateDashedPen(AccentBrush), frame);
|
||||
dc.DrawLine(pen, new Point(frame.X + frame.Width / 2, frame.Top), new Point(frame.X + frame.Width / 2, frame.Top - 22 / ViewScale));
|
||||
dc.Pop();
|
||||
if (RotateHandle(item) is Point rotate)
|
||||
{
|
||||
dc.DrawEllipse(Brushes.White, pen, rotate, size / 2, size / 2);
|
||||
dc.DrawEllipse(AccentBrush, null, rotate, size / 5, size / 5);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Rect bounds = item.Bounds;
|
||||
bounds.Inflate(pad, pad);
|
||||
dc.DrawRectangle(null, CreateDashedPen(AccentBrush), bounds);
|
||||
}
|
||||
|
||||
IReadOnlyList<Point> handles = item.GetHandles();
|
||||
if (handles.Count == 0)
|
||||
@@ -73,8 +188,6 @@ public sealed class AnnotationSurface : FrameworkElement
|
||||
return;
|
||||
}
|
||||
|
||||
double size = HandleSize;
|
||||
var pen = new Pen(AccentBrush, 1.2 / ViewScale);
|
||||
for (int i = 0; i < handles.Count; i++)
|
||||
{
|
||||
Point h = handles[i];
|
||||
@@ -90,6 +203,22 @@ public sealed class AnnotationSurface : FrameworkElement
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Blokjespatroon voor transparantie; de blokjes blijven op elk zoomniveau even groot op het scherm.</summary>
|
||||
private Brush CreateChecker()
|
||||
{
|
||||
double cell = 8 / ViewScale;
|
||||
var drawing = new DrawingGroup();
|
||||
drawing.Children.Add(new GeometryDrawing(CheckerLight, null, new RectangleGeometry(new Rect(0, 0, 2, 2))));
|
||||
drawing.Children.Add(new GeometryDrawing(CheckerDark, null, Geometry.Parse("M0,0 H1 V1 H0 Z M1,1 H2 V2 H1 Z")));
|
||||
return new DrawingBrush(drawing)
|
||||
{
|
||||
TileMode = TileMode.Tile,
|
||||
Viewport = new Rect(0, 0, cell * 2, cell * 2),
|
||||
ViewportUnits = BrushMappingMode.Absolute,
|
||||
Stretch = Stretch.Fill
|
||||
};
|
||||
}
|
||||
|
||||
private Pen CreateDashedPen(Brush brush, double offset = 0) => new(brush, 1 / ViewScale)
|
||||
{
|
||||
DashStyle = new DashStyle(new[] { 4.0, 4.0 }, offset)
|
||||
|
||||
@@ -46,6 +46,67 @@ public abstract class Annotation
|
||||
|
||||
public virtual bool RemoveHandle(int index) => false;
|
||||
|
||||
/// <summary>False voor objecten die gespiegeld hetzelfde of onleesbaar zouden zijn, zoals rechthoeken en tekst.</summary>
|
||||
public virtual bool CanFlip => false;
|
||||
|
||||
/// <summary>Spiegelt het object; de positie mag daarbij verschuiven, de editor zet het daarna terug op zijn plek.</summary>
|
||||
public virtual void Flip(bool horizontal)
|
||||
{
|
||||
}
|
||||
|
||||
protected static Point Mirror(Point p, bool horizontal) => horizontal ? new Point(-p.X, p.Y) : new Point(p.X, -p.Y);
|
||||
|
||||
/// <summary>Te draaien met de draaigreep boven het object.</summary>
|
||||
public virtual bool CanRotate => false;
|
||||
|
||||
/// <summary>Draaiing in graden rechtsom, rond het midden van <see cref="Frame"/>.</summary>
|
||||
public double Angle { get; set; }
|
||||
|
||||
/// <summary>Het kader van het object zonder draaiing; bij draaibare objecten is <see cref="Bounds"/> het omhullende kader daarvan.</summary>
|
||||
public virtual Rect Frame => Bounds;
|
||||
|
||||
public Point Pivot
|
||||
{
|
||||
get
|
||||
{
|
||||
Rect f = Frame;
|
||||
return new Point(f.X + f.Width / 2, f.Y + f.Height / 2);
|
||||
}
|
||||
}
|
||||
|
||||
public Matrix RotationMatrix
|
||||
{
|
||||
get
|
||||
{
|
||||
Matrix m = Matrix.Identity;
|
||||
if (Angle != 0)
|
||||
{
|
||||
Point c = Pivot;
|
||||
m.RotateAt(Angle, c.X, c.Y);
|
||||
}
|
||||
|
||||
return m;
|
||||
}
|
||||
}
|
||||
|
||||
protected Rect RotatedBounds(Rect frame) => Angle == 0 ? frame : Rect.Transform(frame, RotationMatrix);
|
||||
|
||||
/// <summary>Hoek tussen -180 en 180 graden.</summary>
|
||||
public static double NormalizeAngle(double angle)
|
||||
{
|
||||
angle %= 360;
|
||||
if (angle > 180)
|
||||
{
|
||||
angle -= 360;
|
||||
}
|
||||
else if (angle <= -180)
|
||||
{
|
||||
angle += 360;
|
||||
}
|
||||
|
||||
return Math.Abs(angle) < 1e-9 ? 0 : angle;
|
||||
}
|
||||
|
||||
/// <summary>Te klein om te bewaren na het tekenen (bijv. een losse klik met het rechthoek-gereedschap).</summary>
|
||||
public virtual bool IsDegenerate => false;
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ public sealed class BlurAnnotation : RectAnnotation
|
||||
return;
|
||||
}
|
||||
|
||||
var key = (area, Style.BlurMode, Style.BlurStrength, document.Image);
|
||||
var key = (area, Style.BlurMode, Style.BlurStrength, document.EffectImage);
|
||||
if (_cache == null || !_cacheKey.Equals(key))
|
||||
{
|
||||
_cache = Style.BlurMode == BlurMode.Pixelate
|
||||
|
||||
@@ -10,6 +10,11 @@ public sealed class CursorAnnotation : Annotation
|
||||
|
||||
public Point Position { get; set; }
|
||||
|
||||
/// <summary>Gespiegeld rond de hotspot.</summary>
|
||||
public bool FlipX { get; set; }
|
||||
|
||||
public bool FlipY { get; set; }
|
||||
|
||||
public override ToolKind Kind => ToolKind.Cursor;
|
||||
|
||||
private double Scale => Style.Size / DesignSize;
|
||||
@@ -19,8 +24,10 @@ public sealed class CursorAnnotation : Annotation
|
||||
get
|
||||
{
|
||||
Rect r = CursorGeometries.For(Style.CursorKind).Bounds;
|
||||
double x = FlipX ? -r.Right : r.X;
|
||||
double y = FlipY ? -r.Bottom : r.Y;
|
||||
double s = Scale;
|
||||
return new Rect(Position.X + r.X * s, Position.Y + r.Y * s, r.Width * s, r.Height * s);
|
||||
return new Rect(Position.X + x * s, Position.Y + y * s, r.Width * s, r.Height * s);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,21 +36,51 @@ public sealed class CursorAnnotation : Annotation
|
||||
dc.PushTransform(new TranslateTransform(Position.X, Position.Y));
|
||||
dc.PushTransform(new ScaleTransform(Scale, Scale));
|
||||
|
||||
// De geometrie spiegelen in plaats van de hele tekening, zodat de schaduw rechtsonder blijft vallen.
|
||||
CursorGeometries.Parts parts = CursorGeometries.PartsFor(Style.CursorKind);
|
||||
RenderHelpers.DrawWithShadow(dc, parts.Body, Brushes.White, CursorGeometries.OutlinePen, Style.Shadow);
|
||||
RenderHelpers.DrawWithShadow(dc, Mirrored(parts.Body), Brushes.White, CursorGeometries.OutlinePen, Style.Shadow);
|
||||
if (parts.Details != null)
|
||||
{
|
||||
dc.DrawGeometry(null, CursorGeometries.DetailPen, parts.Details);
|
||||
dc.DrawGeometry(null, CursorGeometries.DetailPen, Mirrored(parts.Details));
|
||||
}
|
||||
|
||||
if (parts.Rays != null)
|
||||
{
|
||||
dc.DrawGeometry(null, CursorGeometries.RayPen, parts.Rays);
|
||||
dc.DrawGeometry(null, CursorGeometries.RayPen, Mirrored(parts.Rays));
|
||||
}
|
||||
|
||||
dc.Pop();
|
||||
dc.Pop();
|
||||
}
|
||||
|
||||
private Geometry Mirrored(Geometry geometry)
|
||||
{
|
||||
if (!FlipX && !FlipY)
|
||||
{
|
||||
return geometry;
|
||||
}
|
||||
|
||||
PathGeometry copy = PathGeometry.CreateFromGeometry(geometry);
|
||||
Matrix matrix = copy.Transform?.Value ?? Matrix.Identity;
|
||||
matrix.Scale(FlipX ? -1 : 1, FlipY ? -1 : 1);
|
||||
copy.Transform = new MatrixTransform(matrix);
|
||||
copy.Freeze();
|
||||
return copy;
|
||||
}
|
||||
|
||||
public override void Move(Vector delta) => Position += delta;
|
||||
|
||||
public override bool CanFlip => true;
|
||||
|
||||
public override void Flip(bool horizontal)
|
||||
{
|
||||
if (horizontal)
|
||||
{
|
||||
FlipX = !FlipX;
|
||||
}
|
||||
else
|
||||
{
|
||||
FlipY = !FlipY;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,9 +13,15 @@ public sealed class EmojiAnnotation : Annotation
|
||||
|
||||
public Point Center { get; set; }
|
||||
|
||||
public bool FlipX { get; set; }
|
||||
|
||||
public bool FlipY { get; set; }
|
||||
|
||||
public override ToolKind Kind => ToolKind.Emoji;
|
||||
|
||||
public override Rect Bounds
|
||||
public override bool CanRotate => true;
|
||||
|
||||
public override Rect Frame
|
||||
{
|
||||
get
|
||||
{
|
||||
@@ -24,15 +30,34 @@ public sealed class EmojiAnnotation : Annotation
|
||||
}
|
||||
}
|
||||
|
||||
public override Rect Bounds => RotatedBounds(Frame);
|
||||
|
||||
public override bool HitTest(Point point, double tolerance)
|
||||
{
|
||||
Matrix toLocal = RotationMatrix;
|
||||
toLocal.Invert();
|
||||
Rect frame = Frame;
|
||||
frame.Inflate(tolerance, tolerance);
|
||||
return frame.Contains(toLocal.Transform(point));
|
||||
}
|
||||
|
||||
public override void Render(DrawingContext dc, EditorDocument document)
|
||||
{
|
||||
ImageSource image = EmojiCatalog.Get(Style.Emoji, Style.SkinTone);
|
||||
Rect bounds = Bounds;
|
||||
Rect bounds = Frame;
|
||||
bool flipped = FlipX || FlipY;
|
||||
double sx = FlipX ? -1 : 1, sy = FlipY ? -1 : 1;
|
||||
|
||||
if (Style.Shadow)
|
||||
{
|
||||
double offset = Math.Max(2, Style.Size / 24);
|
||||
var mask = RenderHelpers.Freeze(new ImageBrush(image));
|
||||
var mask = new ImageBrush(image);
|
||||
if (flipped)
|
||||
{
|
||||
mask.RelativeTransform = new ScaleTransform(sx, sy, 0.5, 0.5);
|
||||
}
|
||||
|
||||
mask.Freeze();
|
||||
DrawShadowLayer(dc, mask, bounds, ShadowFar, offset * 1.6);
|
||||
DrawShadowLayer(dc, mask, bounds, ShadowNear, offset);
|
||||
}
|
||||
@@ -42,18 +67,48 @@ public sealed class EmojiAnnotation : Annotation
|
||||
RenderOptions.SetBitmapScalingMode(group, BitmapScalingMode.HighQuality);
|
||||
using (DrawingContext gc = group.Open())
|
||||
{
|
||||
if (Angle != 0)
|
||||
{
|
||||
gc.PushTransform(new RotateTransform(Angle, Center.X, Center.Y));
|
||||
}
|
||||
|
||||
if (flipped)
|
||||
{
|
||||
gc.PushTransform(new ScaleTransform(sx, sy, Center.X, Center.Y));
|
||||
}
|
||||
|
||||
gc.DrawImage(image, bounds);
|
||||
}
|
||||
|
||||
dc.DrawDrawing(group);
|
||||
}
|
||||
|
||||
private static void DrawShadowLayer(DrawingContext dc, Brush mask, Rect bounds, Brush brush, double offset)
|
||||
public override bool CanFlip => true;
|
||||
|
||||
public override void Flip(bool horizontal)
|
||||
{
|
||||
bounds.Offset(offset, offset);
|
||||
if (horizontal)
|
||||
{
|
||||
FlipX = !FlipX;
|
||||
}
|
||||
else
|
||||
{
|
||||
FlipY = !FlipY;
|
||||
}
|
||||
|
||||
Angle = NormalizeAngle(-Angle);
|
||||
}
|
||||
|
||||
/// <summary>De schaduw draait mee met de emoji, maar valt altijd rechtsonder.</summary>
|
||||
private void DrawShadowLayer(DrawingContext dc, Brush mask, Rect bounds, Brush brush, double offset)
|
||||
{
|
||||
dc.PushTransform(new TranslateTransform(offset, offset));
|
||||
dc.PushTransform(new RotateTransform(Angle, Center.X, Center.Y));
|
||||
dc.PushOpacityMask(mask);
|
||||
dc.DrawRectangle(brush, null, bounds);
|
||||
dc.Pop();
|
||||
dc.Pop();
|
||||
dc.Pop();
|
||||
}
|
||||
|
||||
public override void Move(Vector delta) => Center += delta;
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Windows;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace Kniip.Editor.Annotations;
|
||||
|
||||
/// <summary>Een vorm uit het vormengereedschap (driehoek, ster, hart, ...), geschaald naar zijn kader.</summary>
|
||||
public sealed class FigureAnnotation : RectAnnotation
|
||||
{
|
||||
public bool FlipX { get; set; }
|
||||
|
||||
public bool FlipY { get; set; }
|
||||
|
||||
public override ToolKind Kind => ToolKind.Shape;
|
||||
|
||||
public override bool CanRotate => true;
|
||||
|
||||
public override Rect Frame => Rect;
|
||||
|
||||
public override Rect Bounds => RotatedBounds(Rect);
|
||||
|
||||
private bool IsOpen => ShapeGeometries.IsOpen(Style.ShapeKind);
|
||||
|
||||
/// <summary>De vorm in beeldcoördinaten; via de geometrietransformatie, zodat de lijndikte niet meeschaalt.</summary>
|
||||
private Geometry BuildGeometry()
|
||||
{
|
||||
Geometry source = ShapeGeometries.For(Style.ShapeKind);
|
||||
Rect b = source.Bounds;
|
||||
Rect r = Rect;
|
||||
double cx = r.Left + r.Width / 2, cy = r.Top + r.Height / 2;
|
||||
var m = Matrix.Identity;
|
||||
m.Translate(-(b.Left + b.Width / 2), -(b.Top + b.Height / 2));
|
||||
m.Scale(r.Width / b.Width * (FlipX ? -1 : 1), r.Height / b.Height * (FlipY ? -1 : 1));
|
||||
m.Translate(cx, cy);
|
||||
m.RotateAt(Angle, cx, cy);
|
||||
|
||||
Geometry geometry = source.Clone();
|
||||
geometry.Transform = new MatrixTransform(m);
|
||||
geometry.Freeze();
|
||||
return geometry;
|
||||
}
|
||||
|
||||
public override void Render(DrawingContext dc, EditorDocument document)
|
||||
{
|
||||
Rect r = Rect;
|
||||
if (r.Width < 1 && r.Height < 1)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Brush? fill = IsOpen ? null : RenderHelpers.BrushOrNull(Style.Fill);
|
||||
RenderHelpers.DrawWithShadow(dc, BuildGeometry(), fill,
|
||||
RenderHelpers.CreatePen(Style.Stroke, Style.Thickness, Style.Dash), Style.Shadow);
|
||||
}
|
||||
|
||||
public override bool HitTest(Point point, double tolerance)
|
||||
{
|
||||
Geometry geometry = BuildGeometry();
|
||||
if (!IsOpen && geometry.FillContains(point))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var pen = new Pen(Brushes.Black, Style.Thickness + 2 * tolerance);
|
||||
return geometry.StrokeContains(pen, point);
|
||||
}
|
||||
|
||||
public override IReadOnlyList<Point> GetHandles()
|
||||
{
|
||||
Matrix m = RotationMatrix;
|
||||
return base.GetHandles().Select(p => m.Transform(p)).ToArray();
|
||||
}
|
||||
|
||||
/// <summary>Rekent de muis terug naar het ongedraaide kader; daarna wordt het kader zo verschoven dat de tegenoverliggende greep op zijn plek blijft.</summary>
|
||||
public override void MoveHandle(int index, Point p, bool constrain)
|
||||
{
|
||||
Rect o = DragOrigin;
|
||||
var c = new Point(o.Left + o.Width / 2, o.Top + o.Height / 2);
|
||||
Matrix toLocal = Matrix.Identity;
|
||||
toLocal.RotateAt(-Angle, c.X, c.Y);
|
||||
base.MoveHandle(index, toLocal.Transform(p), constrain);
|
||||
|
||||
Rect r = Rect;
|
||||
var rc = new Point(r.Left + r.Width / 2, r.Top + r.Height / 2);
|
||||
Matrix toWorld = Matrix.Identity;
|
||||
toWorld.RotateAt(Angle, c.X, c.Y);
|
||||
Rect = Rect.Offset(r, toWorld.Transform(rc) - rc);
|
||||
}
|
||||
|
||||
public override bool CanFlip => true;
|
||||
|
||||
public override void Flip(bool horizontal)
|
||||
{
|
||||
if (horizontal)
|
||||
{
|
||||
FlipX = !FlipX;
|
||||
}
|
||||
else
|
||||
{
|
||||
FlipY = !FlipY;
|
||||
}
|
||||
|
||||
Angle = NormalizeAngle(-Angle);
|
||||
}
|
||||
}
|
||||
@@ -111,6 +111,16 @@ public sealed class HighlightAnnotation : Annotation
|
||||
Points[index] = new Point(point.X, Points[1 - index].Y);
|
||||
}
|
||||
|
||||
public override bool CanFlip => !Straight;
|
||||
|
||||
public override void Flip(bool horizontal)
|
||||
{
|
||||
for (int i = 0; i < Points.Count; i++)
|
||||
{
|
||||
Points[i] = Mirror(Points[i], horizontal);
|
||||
}
|
||||
}
|
||||
|
||||
public override Annotation Clone()
|
||||
{
|
||||
var clone = (HighlightAnnotation)base.Clone();
|
||||
|
||||
@@ -280,6 +280,18 @@ public sealed class LineAnnotation : Annotation
|
||||
return true;
|
||||
}
|
||||
|
||||
public override bool CanFlip => true;
|
||||
|
||||
public override void Flip(bool horizontal)
|
||||
{
|
||||
Start = Mirror(Start, horizontal);
|
||||
End = Mirror(End, horizontal);
|
||||
for (int i = 0; i < Bends.Count; i++)
|
||||
{
|
||||
Bends[i] = Mirror(Bends[i], horizontal);
|
||||
}
|
||||
}
|
||||
|
||||
public override Annotation Clone()
|
||||
{
|
||||
var clone = (LineAnnotation)base.Clone();
|
||||
|
||||
@@ -6,7 +6,7 @@ namespace Kniip.Editor.Annotations;
|
||||
/// <summary>Basis voor annotaties die door een rechthoek worden beschreven, met 8 formaatgrepen.</summary>
|
||||
public abstract class RectAnnotation : Annotation
|
||||
{
|
||||
private Rect _dragOrigin;
|
||||
protected Rect DragOrigin { get; private set; }
|
||||
|
||||
public Rect Rect { get; set; }
|
||||
|
||||
@@ -28,11 +28,11 @@ public abstract class RectAnnotation : Annotation
|
||||
};
|
||||
}
|
||||
|
||||
public override void BeginHandleDrag(int index) => _dragOrigin = Rect;
|
||||
public override void BeginHandleDrag(int index) => DragOrigin = Rect;
|
||||
|
||||
public override void MoveHandle(int index, Point p, bool constrain)
|
||||
{
|
||||
Rect o = _dragOrigin;
|
||||
Rect o = DragOrigin;
|
||||
Rect = index switch
|
||||
{
|
||||
0 => FromAnchor(o.BottomRight, p, constrain),
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Windows;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace Kniip.Editor.Annotations;
|
||||
|
||||
/// <summary>Vormen voor het vormengereedschap, ontworpen in een raster van 100 x 100 en bij het tekenen naar het kader geschaald.</summary>
|
||||
internal static class ShapeGeometries
|
||||
{
|
||||
private static readonly Dictionary<ShapeKind, Geometry> Cache = new();
|
||||
|
||||
public static bool IsOpen(ShapeKind kind) => kind == ShapeKind.Spiral;
|
||||
|
||||
public static Geometry For(ShapeKind kind)
|
||||
{
|
||||
if (!Cache.TryGetValue(kind, out Geometry? geometry))
|
||||
{
|
||||
geometry = RenderHelpers.Freeze(Create(kind));
|
||||
Cache[kind] = geometry;
|
||||
}
|
||||
|
||||
return geometry;
|
||||
}
|
||||
|
||||
private static Geometry Create(ShapeKind kind) => kind switch
|
||||
{
|
||||
ShapeKind.BlockArrow => Geometry.Parse("M0,30 L58,30 L58,4 L100,50 L58,96 L58,70 L0,70 Z"),
|
||||
ShapeKind.Star => Star(),
|
||||
ShapeKind.Cloud => Cloud(),
|
||||
ShapeKind.Balloon => Balloon(),
|
||||
ShapeKind.Heart => Geometry.Parse(
|
||||
"M50,95 C14,68 0,48 0,28 C0,11 13,0 28,0 C39,0 46,7 50,15 C54,7 61,0 72,0 C87,0 100,11 100,28 C100,48 86,68 50,95 Z"),
|
||||
ShapeKind.Moon => Geometry.Combine(
|
||||
new EllipseGeometry(new Point(50, 50), 50, 50),
|
||||
new EllipseGeometry(new Point(74, 38), 42, 42),
|
||||
GeometryCombineMode.Exclude, null, 0.01, ToleranceType.Absolute),
|
||||
ShapeKind.Drop => Geometry.Parse("M50,0 C58,16 85,44 85,65 A35,35 0 1 1 15,65 C15,44 42,16 50,0 Z"),
|
||||
ShapeKind.Egg => Geometry.Parse("M50,0 C77,0 94,38 94,62 C94,86 74,100 50,100 C26,100 6,86 6,62 C6,38 23,0 50,0 Z"),
|
||||
ShapeKind.Lightning => Geometry.Parse("M58,0 L14,56 L44,56 L30,100 L86,38 L56,38 L74,0 Z"),
|
||||
ShapeKind.Spiral => Spiral(),
|
||||
_ => Geometry.Parse("M50,0 L100,100 L0,100 Z")
|
||||
};
|
||||
|
||||
private static Geometry Star()
|
||||
{
|
||||
var points = new Point[10];
|
||||
for (int i = 0; i < points.Length; i++)
|
||||
{
|
||||
double angle = -Math.PI / 2 + i * Math.PI / 5;
|
||||
double radius = i % 2 == 0 ? 50 : 20;
|
||||
points[i] = new Point(50 + radius * Math.Cos(angle), 50 + radius * Math.Sin(angle));
|
||||
}
|
||||
|
||||
return Polygon(points, closed: true);
|
||||
}
|
||||
|
||||
private static Geometry Cloud()
|
||||
{
|
||||
var center = new Point(50, 50);
|
||||
const double rx = 33, ry = 19, bump = 19;
|
||||
const int count = 6;
|
||||
Geometry cloud = new EllipseGeometry(center, rx, ry);
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
double angle = 2 * Math.PI * (i + 0.5) / count;
|
||||
var p = new Point(center.X + rx * Math.Cos(angle), center.Y + ry * Math.Sin(angle));
|
||||
cloud = Geometry.Combine(cloud, new EllipseGeometry(p, bump, bump), GeometryCombineMode.Union, null, 0.01, ToleranceType.Absolute);
|
||||
}
|
||||
|
||||
return cloud;
|
||||
}
|
||||
|
||||
private static Geometry Balloon() =>
|
||||
Geometry.Combine(
|
||||
new RectangleGeometry(new Rect(0, 0, 100, 72), 14, 14),
|
||||
Geometry.Parse("M20,68 L10,100 L42,68 Z"),
|
||||
GeometryCombineMode.Union, null, 0.01, ToleranceType.Absolute);
|
||||
|
||||
private static Geometry Spiral()
|
||||
{
|
||||
const double turns = 3;
|
||||
const int steps = 240;
|
||||
double maxAngle = turns * 2 * Math.PI;
|
||||
var points = new Point[steps + 1];
|
||||
for (int i = 0; i <= steps; i++)
|
||||
{
|
||||
double angle = maxAngle * i / steps;
|
||||
double radius = 50 * angle / maxAngle;
|
||||
points[i] = new Point(50 + radius * Math.Cos(angle), 50 + radius * Math.Sin(angle));
|
||||
}
|
||||
|
||||
return Polygon(points, closed: false);
|
||||
}
|
||||
|
||||
private static Geometry Polygon(IReadOnlyList<Point> points, bool closed)
|
||||
{
|
||||
var geometry = new StreamGeometry();
|
||||
using (StreamGeometryContext ctx = geometry.Open())
|
||||
{
|
||||
ctx.BeginFigure(points[0], closed, closed);
|
||||
for (int i = 1; i < points.Count; i++)
|
||||
{
|
||||
ctx.LineTo(points[i], true, true);
|
||||
}
|
||||
}
|
||||
|
||||
return geometry;
|
||||
}
|
||||
}
|
||||
@@ -1,34 +1,136 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Windows;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace Kniip.Editor.Annotations;
|
||||
|
||||
/// <summary>Tekst in een kader met achtergrond, rand en afgeronde hoeken.</summary>
|
||||
/// <summary>
|
||||
/// Tekst in een kader, ballon, ovaal of wolk. Ballon, ovaal en wolk hebben een staart naar <see cref="TailTip"/>.
|
||||
/// Zonder <see cref="BoxWidth"/> past de vorm zich aan de tekst aan; met een vaste breedte loopt de tekst door naar de volgende regel.
|
||||
/// </summary>
|
||||
public sealed class TextAnnotation : Annotation
|
||||
{
|
||||
private const int TailHandle = 8;
|
||||
|
||||
private Rect _dragOrigin;
|
||||
private ShapeKey _cachedKey;
|
||||
private Geometry? _cachedShape;
|
||||
|
||||
public Point Position { get; set; }
|
||||
|
||||
public string Text { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Vaste breedte van de vorm (zonder staart); null = passend om de tekst.</summary>
|
||||
public double? BoxWidth { get; set; }
|
||||
|
||||
/// <summary>Minimale hoogte van de vorm; past de tekst er niet in, dan wordt hij hoger.</summary>
|
||||
public double? BoxHeight { get; set; }
|
||||
|
||||
/// <summary>Punt waar de staart naar wijst, in afbeeldingspixels; null = standaard linksonder de vorm.</summary>
|
||||
public Point? TailTip { get; set; }
|
||||
|
||||
public override ToolKind Kind => ToolKind.Text;
|
||||
|
||||
public double Padding => Math.Round(Math.Max(4, Style.FontSize * 0.4)) + Math.Max(0, Style.Thickness) / 2;
|
||||
|
||||
public bool HasTail => Style.TextShape != TextShape.Frame;
|
||||
|
||||
/// <summary>Straal van de bolletjes langs de rand van een wolk.</summary>
|
||||
private double BumpRadius => Math.Max(6, Style.FontSize * 0.7);
|
||||
|
||||
private readonly record struct Layout(Rect Box, Rect Area, FormattedText Text, Point Origin);
|
||||
|
||||
private readonly record struct ShapeKey(Rect Box, Point Tip, TextShape Shape, double Corner, double Bump, double Thickness, int Bubbles);
|
||||
|
||||
/// <summary>Van de breedte of hoogte van het tekstvlak naar die van de vorm.</summary>
|
||||
private double ToBox(double area) => Style.TextShape switch
|
||||
{
|
||||
TextShape.Oval => area * Math.Sqrt(2),
|
||||
TextShape.Cloud => area * Math.Sqrt(2) + 2 * BumpRadius,
|
||||
_ => area
|
||||
};
|
||||
|
||||
private double ToArea(double box) => Style.TextShape switch
|
||||
{
|
||||
TextShape.Oval => box / Math.Sqrt(2),
|
||||
TextShape.Cloud => Math.Max(0, box - 2 * BumpRadius) / Math.Sqrt(2),
|
||||
_ => box
|
||||
};
|
||||
|
||||
public double MinBoxWidth => ToBox(Style.FontSize * 2 + 2 * Padding);
|
||||
|
||||
private FormattedText Format(string text)
|
||||
{
|
||||
FormattedText ft = RenderHelpers.FormatText(text, Style.FontSize, Style.Foreground, Style.Bold);
|
||||
ft.Trimming = TextTrimming.None;
|
||||
return ft;
|
||||
}
|
||||
|
||||
private Layout GetLayout()
|
||||
{
|
||||
FormattedText ft = Format(Text.Length == 0 ? " " : Text);
|
||||
double pad = Padding;
|
||||
double boxWidth;
|
||||
if (BoxWidth is double fixedWidth)
|
||||
{
|
||||
boxWidth = Math.Max(fixedWidth, MinBoxWidth);
|
||||
ft.MaxTextWidth = Math.Max(1, ToArea(boxWidth) - 2 * pad + 0.01);
|
||||
}
|
||||
else
|
||||
{
|
||||
boxWidth = ToBox(ft.WidthIncludingTrailingWhitespace + 2 * pad);
|
||||
}
|
||||
|
||||
double boxHeight = Math.Max(BoxHeight ?? 0, ToBox(ft.Height + 2 * pad));
|
||||
var box = new Rect(Position, new Size(boxWidth, boxHeight));
|
||||
double areaWidth = ToArea(boxWidth);
|
||||
double areaHeight = ToArea(boxHeight);
|
||||
var area = new Rect(box.X + (boxWidth - areaWidth) / 2, box.Y + (boxHeight - areaHeight) / 2, areaWidth, areaHeight);
|
||||
var origin = new Point(area.X + pad, area.Y + (area.Height - ft.Height) / 2);
|
||||
return new Layout(box, area, ft, origin);
|
||||
}
|
||||
|
||||
/// <summary>De vorm zonder staart; hier zitten de formaatgrepen.</summary>
|
||||
public Rect Box => GetLayout().Box;
|
||||
|
||||
/// <summary>Linksboven van de eerste regel tekst.</summary>
|
||||
public Point TextOrigin => GetLayout().Origin;
|
||||
|
||||
/// <summary>Breedte waarop de tekst afbreekt, of null als de vorm met de tekst meegroeit.</summary>
|
||||
public double? WrapWidth => BoxWidth == null ? null : GetLayout().Text.MaxTextWidth;
|
||||
|
||||
public Point DefaultTailTip(Rect box) =>
|
||||
new(box.Left - Style.FontSize, box.Bottom + Style.FontSize * 2);
|
||||
|
||||
private Point Tip(Rect box) => TailTip ?? DefaultTailTip(box);
|
||||
|
||||
public override Rect Bounds
|
||||
{
|
||||
get
|
||||
{
|
||||
FormattedText ft = Format(Text.Length == 0 ? " " : Text);
|
||||
double pad = Padding;
|
||||
return new Rect(Position, new Size(ft.WidthIncludingTrailingWhitespace + 2 * pad, ft.Height + 2 * pad));
|
||||
Rect box = GetLayout().Box;
|
||||
Rect r = ShapeFor(box).Bounds;
|
||||
r.Union(box);
|
||||
double half = Math.Max(0, Style.Thickness) / 2;
|
||||
r.Inflate(half, half);
|
||||
return r;
|
||||
}
|
||||
}
|
||||
|
||||
public override bool IsDegenerate => string.IsNullOrWhiteSpace(Text);
|
||||
|
||||
private FormattedText Format(string text) =>
|
||||
RenderHelpers.FormatText(text, Style.FontSize, Style.Foreground, Style.Bold);
|
||||
public override bool HitTest(Point point, double tolerance)
|
||||
{
|
||||
Geometry shape = ShapeFor(GetLayout().Box);
|
||||
if (shape.FillContains(point))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var pen = new Pen(Brushes.Black, 2 * tolerance + Math.Max(0, Style.Thickness));
|
||||
return shape.StrokeContains(pen, point);
|
||||
}
|
||||
|
||||
public override void Render(DrawingContext dc, EditorDocument document)
|
||||
{
|
||||
@@ -37,25 +139,307 @@ public sealed class TextAnnotation : Annotation
|
||||
return;
|
||||
}
|
||||
|
||||
FormattedText ft = Format(Text);
|
||||
double pad = Padding;
|
||||
var box = new Rect(Position, new Size(ft.WidthIncludingTrailingWhitespace + 2 * pad, ft.Height + 2 * pad));
|
||||
|
||||
Brush? fill = RenderHelpers.BrushOrNull(Style.Fill);
|
||||
Pen? pen = RenderHelpers.CreatePen(Style.Stroke, Style.Thickness, Style.Dash);
|
||||
if (fill != null || pen != null)
|
||||
{
|
||||
RenderHelpers.DrawWithShadow(dc, new RectangleGeometry(box, Style.CornerRadius, Style.CornerRadius),
|
||||
fill, pen, Style.Shadow);
|
||||
}
|
||||
else if (Style.Shadow)
|
||||
Layout layout = GetLayout();
|
||||
bool hasShape = RenderShape(dc, layout.Box);
|
||||
if (!hasShape && Style.Shadow)
|
||||
{
|
||||
FormattedText shadow = RenderHelpers.FormatText(Text, Style.FontSize, Color.FromArgb(90, 0, 0, 0), Style.Bold);
|
||||
dc.DrawText(shadow, new Point(Position.X + pad + 1.5, Position.Y + pad + 1.5));
|
||||
shadow.Trimming = TextTrimming.None;
|
||||
if (BoxWidth != null)
|
||||
{
|
||||
shadow.MaxTextWidth = layout.Text.MaxTextWidth;
|
||||
}
|
||||
|
||||
dc.DrawText(shadow, new Point(layout.Origin.X + 1.5, layout.Origin.Y + 1.5));
|
||||
}
|
||||
|
||||
dc.DrawText(ft, new Point(Position.X + pad, Position.Y + pad));
|
||||
dc.DrawText(layout.Text, layout.Origin);
|
||||
}
|
||||
|
||||
public override void Move(Vector delta) => Position += delta;
|
||||
/// <summary>Alleen de vorm, zonder tekst; wordt getekend terwijl de tekst in een tekstvak wordt bewerkt.</summary>
|
||||
public void RenderOutline(DrawingContext dc) => RenderShape(dc, GetLayout().Box);
|
||||
|
||||
/// <summary>True als er een rand of vulling is getekend.</summary>
|
||||
private bool RenderShape(DrawingContext dc, Rect box)
|
||||
{
|
||||
Brush? fill = RenderHelpers.BrushOrNull(Style.Fill);
|
||||
Pen? pen = RenderHelpers.CreatePen(Style.Stroke, Style.Thickness, Style.Dash);
|
||||
if (fill == null && pen == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
RenderHelpers.DrawWithShadow(dc, ShapeFor(box), fill, pen, Style.Shadow);
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Vorm
|
||||
|
||||
private Geometry ShapeFor(Rect box)
|
||||
{
|
||||
var key = new ShapeKey(box, HasTail ? Tip(box) : default, Style.TextShape, Style.CornerRadius, BumpRadius, Style.Thickness, Style.CloudBubbles);
|
||||
if (_cachedShape == null || key != _cachedKey)
|
||||
{
|
||||
_cachedShape = BuildShape(box, key.Tip);
|
||||
_cachedKey = key;
|
||||
}
|
||||
|
||||
return _cachedShape;
|
||||
}
|
||||
|
||||
private Geometry BuildShape(Rect box, Point tip)
|
||||
{
|
||||
Geometry shape = Style.TextShape switch
|
||||
{
|
||||
TextShape.Balloon => WithTail(new RectangleGeometry(box, Style.CornerRadius, Style.CornerRadius), BalloonTail(box, tip)),
|
||||
TextShape.Oval => WithTail(new EllipseGeometry(box), OvalTail(box, tip)),
|
||||
TextShape.Cloud => Cloud(box, tip),
|
||||
_ => new RectangleGeometry(box, Style.CornerRadius, Style.CornerRadius)
|
||||
};
|
||||
|
||||
shape.Freeze();
|
||||
return shape;
|
||||
}
|
||||
|
||||
private static Geometry WithTail(Geometry body, Geometry? tail) =>
|
||||
tail == null ? body : Geometry.Combine(body, tail, GeometryCombineMode.Union, null);
|
||||
|
||||
private static Geometry Triangle(Point a, Point b, Point tip)
|
||||
{
|
||||
var geometry = new StreamGeometry();
|
||||
using (StreamGeometryContext ctx = geometry.Open())
|
||||
{
|
||||
ctx.BeginFigure(a, true, true);
|
||||
ctx.LineTo(tip, true, true);
|
||||
ctx.LineTo(b, true, true);
|
||||
}
|
||||
|
||||
return geometry;
|
||||
}
|
||||
|
||||
/// <summary>Spitse staart vanaf de zijde die naar de punt kijkt.</summary>
|
||||
private Geometry? BalloonTail(Rect box, Point tip)
|
||||
{
|
||||
if (box.Contains(tip))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
Point center = new(box.X + box.Width / 2, box.Y + box.Height / 2);
|
||||
Vector d = tip - center;
|
||||
bool horizontal = Math.Abs(d.X) / Math.Max(1, box.Width) > Math.Abs(d.Y) / Math.Max(1, box.Height);
|
||||
double corner = Math.Min(Style.CornerRadius, Math.Min(box.Width, box.Height) / 2);
|
||||
double inset = Math.Max(1, Style.Thickness) + 1;
|
||||
|
||||
double side = horizontal ? box.Height : box.Width;
|
||||
double half = Math.Max(1, Math.Min(side * 0.125, (side - 2 * corner) / 2));
|
||||
double low = (horizontal ? box.Top : box.Left) + corner + half;
|
||||
double high = (horizontal ? box.Bottom : box.Right) - corner - half;
|
||||
double middle = horizontal ? center.Y : center.X;
|
||||
double toward = horizontal ? tip.Y : tip.X;
|
||||
double c = low > high ? middle : Math.Clamp((middle + toward) / 2, low, high);
|
||||
|
||||
if (horizontal)
|
||||
{
|
||||
double x = d.X < 0 ? box.Left + inset : box.Right - inset;
|
||||
return Triangle(new Point(x, c - half), new Point(x, c + half), tip);
|
||||
}
|
||||
|
||||
double y = d.Y < 0 ? box.Top + inset : box.Bottom - inset;
|
||||
return Triangle(new Point(c - half, y), new Point(c + half, y), tip);
|
||||
}
|
||||
|
||||
private static Point OnEllipse(Point center, double rx, double ry, double angle) =>
|
||||
new(center.X + rx * Math.Cos(angle), center.Y + ry * Math.Sin(angle));
|
||||
|
||||
private static bool InsideEllipse(Point center, double rx, double ry, Point p)
|
||||
{
|
||||
if (rx <= 0 || ry <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
double x = (p.X - center.X) / rx;
|
||||
double y = (p.Y - center.Y) / ry;
|
||||
return x * x + y * y <= 1;
|
||||
}
|
||||
|
||||
/// <summary>Afstand van een punt tot de ellips, gemeten langs de lijn vanuit het midden; negatief als het punt erbinnen ligt.</summary>
|
||||
private static double OutsideDistance(Point center, double rx, double ry, Point p)
|
||||
{
|
||||
Vector v = p - center;
|
||||
double scale = Math.Sqrt(Math.Pow(v.X / rx, 2) + Math.Pow(v.Y / ry, 2));
|
||||
return scale < 1e-9 ? -Math.Min(rx, ry) : v.Length * (1 - 1 / scale);
|
||||
}
|
||||
|
||||
/// <summary>Hoek op de ellips (in de genormaliseerde ruimte) in de richting van de punt.</summary>
|
||||
private static double AngleToward(Point center, double rx, double ry, Point tip) =>
|
||||
Math.Atan2((tip.Y - center.Y) / Math.Max(1, ry), (tip.X - center.X) / Math.Max(1, rx));
|
||||
|
||||
private Geometry? OvalTail(Rect box, Point tip)
|
||||
{
|
||||
Point center = new(box.X + box.Width / 2, box.Y + box.Height / 2);
|
||||
double rx = box.Width / 2, ry = box.Height / 2;
|
||||
if (InsideEllipse(center, rx, ry, tip))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
double angle = AngleToward(center, rx, ry, tip);
|
||||
const double spread = 0.24;
|
||||
const double inward = 0.94;
|
||||
return Triangle(
|
||||
OnEllipse(center, rx * inward, ry * inward, angle - spread),
|
||||
OnEllipse(center, rx * inward, ry * inward, angle + spread),
|
||||
tip);
|
||||
}
|
||||
|
||||
/// <summary>Bolletjes langs een ellips, met als staart steeds kleinere rondjes naar de punt; automatisch meer rondjes naarmate de punt verder weg ligt.</summary>
|
||||
private Geometry Cloud(Rect box, Point tip)
|
||||
{
|
||||
double r = BumpRadius;
|
||||
Point center = new(box.X + box.Width / 2, box.Y + box.Height / 2);
|
||||
double rx = Math.Max(1, box.Width / 2 - r);
|
||||
double ry = Math.Max(1, box.Height / 2 - r);
|
||||
|
||||
double h = Math.Pow((rx - ry) / (rx + ry), 2);
|
||||
double perimeter = Math.PI * (rx + ry) * (1 + 3 * h / (10 + Math.Sqrt(4 - 3 * h)));
|
||||
int count = Math.Max(6, (int)Math.Round(perimeter / (r * 1.75)));
|
||||
|
||||
Geometry cloud = new EllipseGeometry(center, rx, ry);
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
Point p = OnEllipse(center, rx, ry, 2 * Math.PI * i / count);
|
||||
cloud = Geometry.Combine(cloud, new EllipseGeometry(p, r, r), GeometryCombineMode.Union, null);
|
||||
}
|
||||
|
||||
if (!HasTail || InsideEllipse(center, rx + r, ry + r, tip))
|
||||
{
|
||||
return cloud;
|
||||
}
|
||||
|
||||
var group = new GeometryGroup { FillRule = FillRule.Nonzero };
|
||||
group.Children.Add(cloud);
|
||||
|
||||
// Vanaf de punt terug naar de wolk, gelijk verdeeld; rondjes die de wolk zouden raken vallen weg.
|
||||
Vector back = center - tip;
|
||||
back.Normalize();
|
||||
|
||||
double gap = Math.Max(2, r * 0.35);
|
||||
double smallest = Math.Max(2, r * 0.32);
|
||||
double largest = Math.Max(smallest, r * 0.72);
|
||||
double pitch = 2 * largest + gap;
|
||||
double available = Math.Max(0, OutsideDistance(center, rx + r, ry + r, tip) - gap - largest);
|
||||
int bubbles = Style.CloudBubbles > 0
|
||||
? Style.CloudBubbles
|
||||
: Math.Clamp((int)(available / pitch) + 1, 1, 40);
|
||||
double spacing = bubbles > 1 ? available / (bubbles - 1) : 0;
|
||||
double shrink = bubbles > 1 ? Math.Min(1, spacing / pitch) : 1;
|
||||
|
||||
for (int i = 0; i < bubbles; i++)
|
||||
{
|
||||
double t = bubbles > 1 ? (double)i / (bubbles - 1) : 0.5;
|
||||
double radius = Math.Max(1, (smallest + (largest - smallest) * t) * shrink);
|
||||
Point c = tip + back * (spacing * i);
|
||||
if (OutsideDistance(center, rx + r, ry + r, c) < radius * 0.8)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
group.Children.Add(new EllipseGeometry(c, radius, radius));
|
||||
}
|
||||
|
||||
return group;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Grepen
|
||||
|
||||
public override void Move(Vector delta)
|
||||
{
|
||||
Position += delta;
|
||||
if (TailTip is Point tip)
|
||||
{
|
||||
TailTip = tip + delta;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Volgorde: 8 formaatgrepen zoals bij <see cref="RectAnnotation"/>, daarna de staartpunt.</summary>
|
||||
public override IReadOnlyList<Point> GetHandles()
|
||||
{
|
||||
Rect r = GetLayout().Box;
|
||||
double cx = r.Left + r.Width / 2;
|
||||
double cy = r.Top + r.Height / 2;
|
||||
var handles = new List<Point>
|
||||
{
|
||||
r.TopLeft, new Point(cx, r.Top), r.TopRight, new Point(r.Right, cy),
|
||||
r.BottomRight, new Point(cx, r.Bottom), r.BottomLeft, new Point(r.Left, cy)
|
||||
};
|
||||
|
||||
if (HasTail)
|
||||
{
|
||||
handles.Add(Tip(r));
|
||||
}
|
||||
|
||||
return handles;
|
||||
}
|
||||
|
||||
public override void BeginHandleDrag(int index)
|
||||
{
|
||||
_dragOrigin = GetLayout().Box;
|
||||
TailTip ??= DefaultTailTip(_dragOrigin);
|
||||
}
|
||||
|
||||
public override void MoveHandle(int index, Point p, bool constrain)
|
||||
{
|
||||
if (index == TailHandle)
|
||||
{
|
||||
TailTip = p;
|
||||
return;
|
||||
}
|
||||
|
||||
Rect o = _dragOrigin;
|
||||
Rect r = index switch
|
||||
{
|
||||
0 => RectAnnotation.FromAnchor(o.BottomRight, p, constrain),
|
||||
1 => new Rect(new Point(o.Left, p.Y), o.BottomRight),
|
||||
2 => RectAnnotation.FromAnchor(o.BottomLeft, p, constrain),
|
||||
3 => new Rect(o.TopLeft, new Point(p.X, o.Bottom)),
|
||||
4 => RectAnnotation.FromAnchor(o.TopLeft, p, constrain),
|
||||
5 => new Rect(o.TopLeft, new Point(o.Right, p.Y)),
|
||||
6 => RectAnnotation.FromAnchor(o.TopRight, p, constrain),
|
||||
7 => new Rect(new Point(p.X, o.Top), o.BottomRight),
|
||||
_ => o
|
||||
};
|
||||
|
||||
double minWidth = MinBoxWidth;
|
||||
if (r.Width < minWidth)
|
||||
{
|
||||
bool leftEdge = index is 0 or 6 or 7 && p.X < o.Right;
|
||||
r = new Rect(leftEdge ? r.Right - minWidth : r.Left, r.Top, minWidth, r.Height);
|
||||
}
|
||||
|
||||
Position = r.TopLeft;
|
||||
BoxWidth = r.Width;
|
||||
BoxHeight = r.Height;
|
||||
}
|
||||
|
||||
/// <summary>Dubbelklik op een formaatgreep: terug naar een vorm die met de tekst meegroeit.</summary>
|
||||
public override bool RemoveHandle(int index)
|
||||
{
|
||||
if (index >= TailHandle || (BoxWidth == null && BoxHeight == null))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
BoxWidth = null;
|
||||
BoxHeight = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
public override Annotation Clone()
|
||||
{
|
||||
var clone = (TextAnnotation)base.Clone();
|
||||
clone._cachedShape = null;
|
||||
return clone;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
using System;
|
||||
using System.Windows;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
|
||||
namespace Kniip.Editor;
|
||||
|
||||
public enum BackgroundKind { None, Color, Gradient, Image }
|
||||
|
||||
public enum GradientDirection { Linear, Radial }
|
||||
|
||||
/// <summary>
|
||||
/// Wat er onder het knipsel ligt; alleen zichtbaar waar het knipsel transparant is.
|
||||
/// De kleuren en afbeelding blijven bewaard bij het wisselen van soort.
|
||||
/// </summary>
|
||||
/// <param name="Angle">Richting van een lineair verloop in graden, met de klok mee vanaf boven: 90 = naar rechts, 180 = naar onder.</param>
|
||||
public sealed record CanvasBackground(
|
||||
BackgroundKind Kind,
|
||||
Color Color,
|
||||
Color Color2,
|
||||
GradientDirection Direction,
|
||||
double Angle,
|
||||
BitmapSource? Image)
|
||||
{
|
||||
public static readonly CanvasBackground Default = new(
|
||||
BackgroundKind.None, Colors.White, Color.FromRgb(0x00, 0x78, 0xD4), GradientDirection.Linear, 180, null);
|
||||
|
||||
public bool IsVisible => Kind switch
|
||||
{
|
||||
BackgroundKind.None => false,
|
||||
BackgroundKind.Image => Image != null,
|
||||
_ => true
|
||||
};
|
||||
|
||||
public void Render(DrawingContext dc, int width, int height)
|
||||
{
|
||||
var area = new Rect(0, 0, width, height);
|
||||
switch (Kind)
|
||||
{
|
||||
case BackgroundKind.Color:
|
||||
dc.DrawRectangle(new SolidColorBrush(Color), null, area);
|
||||
break;
|
||||
case BackgroundKind.Gradient:
|
||||
dc.DrawRectangle(CreateGradient(), null, area);
|
||||
break;
|
||||
case BackgroundKind.Image when Image != null:
|
||||
// Eigen schaalmodus: de export tekent verder met NearestNeighbor.
|
||||
var group = new DrawingGroup { ClipGeometry = new RectangleGeometry(area) };
|
||||
RenderOptions.SetBitmapScalingMode(group, BitmapScalingMode.HighQuality);
|
||||
group.Children.Add(new ImageDrawing(Image, Cover(Image.PixelWidth, Image.PixelHeight, area)));
|
||||
dc.DrawDrawing(group);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private Brush CreateGradient()
|
||||
{
|
||||
if (Direction == GradientDirection.Radial)
|
||||
{
|
||||
return new RadialGradientBrush(Color, Color2);
|
||||
}
|
||||
|
||||
// In de eenheidsruimte van het vlak, zodat 45° van hoek naar hoek loopt; de halve lengte reikt tot in de hoeken.
|
||||
double radians = Angle * Math.PI / 180;
|
||||
var direction = new Vector(Math.Sin(radians), -Math.Cos(radians));
|
||||
double half = (Math.Abs(direction.X) + Math.Abs(direction.Y)) / 2;
|
||||
var center = new Point(0.5, 0.5);
|
||||
return new LinearGradientBrush(Color, Color2, center - direction * half, center + direction * half);
|
||||
}
|
||||
|
||||
/// <summary>Afbeelding zo schalen dat ze het hele vlak vult; wat uitsteekt valt weg.</summary>
|
||||
private static Rect Cover(double imageWidth, double imageHeight, Rect area)
|
||||
{
|
||||
double scale = Math.Max(area.Width / imageWidth, area.Height / imageHeight);
|
||||
double w = imageWidth * scale, h = imageHeight * scale;
|
||||
return new Rect(area.X + (area.Width - w) / 2, area.Y + (area.Height - h) / 2, w, h);
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@
|
||||
<DrawingBrush.Drawing>
|
||||
<DrawingGroup>
|
||||
<GeometryDrawing Brush="White" Geometry="M0,0 H2 V2 H0 Z" />
|
||||
<GeometryDrawing Brush="#D4D4D4" Geometry="M0,0 H1 V1 H0 Z M1,1 H2 V2 H1 Z" />
|
||||
<GeometryDrawing Brush="#E4E4E4" Geometry="M0,0 H1 V1 H0 Z M1,1 H2 V2 H1 Z" />
|
||||
</DrawingGroup>
|
||||
</DrawingBrush.Drawing>
|
||||
</DrawingBrush>
|
||||
|
||||
@@ -7,8 +7,13 @@ namespace Kniip.Editor;
|
||||
|
||||
internal static class DocumentRenderer
|
||||
{
|
||||
public static void Render(DrawingContext dc, EditorDocument document)
|
||||
public static void Render(DrawingContext dc, EditorDocument document, bool includeBackground = true)
|
||||
{
|
||||
if (includeBackground && document.HasTransparency)
|
||||
{
|
||||
document.Background.Render(dc, document.Width, document.Height);
|
||||
}
|
||||
|
||||
dc.DrawImage(document.Image, new Rect(0, 0, document.Width, document.Height));
|
||||
foreach (Annotation item in document.Items)
|
||||
{
|
||||
@@ -16,11 +21,16 @@ internal static class DocumentRenderer
|
||||
{
|
||||
item.Render(dc, document);
|
||||
}
|
||||
else if (item is TextAnnotation text)
|
||||
{
|
||||
text.RenderOutline(dc);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Maakt de annotaties plat op de originele resolutie van het knipsel.</summary>
|
||||
public static BitmapSource Export(EditorDocument document)
|
||||
/// <param name="includeBackground">False bij samenvoegen: de achtergrond blijft dan apart instelbaar.</param>
|
||||
public static BitmapSource Export(EditorDocument document, bool includeBackground = true)
|
||||
{
|
||||
var visual = new DrawingVisual();
|
||||
RenderOptions.SetBitmapScalingMode(visual, BitmapScalingMode.NearestNeighbor);
|
||||
@@ -28,7 +38,7 @@ internal static class DocumentRenderer
|
||||
using (DrawingContext dc = visual.RenderOpen())
|
||||
{
|
||||
dc.PushClip(new RectangleGeometry(new Rect(0, 0, document.Width, document.Height)));
|
||||
Render(dc, document);
|
||||
Render(dc, document, includeBackground);
|
||||
dc.Pop();
|
||||
}
|
||||
|
||||
|
||||
@@ -2,20 +2,28 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Windows;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using Kniip.Editor.Annotations;
|
||||
using Kniip.Infrastructure;
|
||||
|
||||
namespace Kniip.Editor;
|
||||
|
||||
public sealed record DocumentState(BitmapSource Image, byte[] Pixels, List<Annotation> Items);
|
||||
public sealed record DocumentState(BitmapSource Image, byte[] Pixels, List<Annotation> Items, CanvasBackground Background);
|
||||
|
||||
/// <summary>Het knipsel (als Bgra32-pixels, 96 dpi zodat 1 DIP = 1 pixel) plus de annotaties erop.</summary>
|
||||
public sealed class EditorDocument
|
||||
{
|
||||
public EditorDocument(BitmapSource source)
|
||||
private (BitmapSource Image, byte[] Pixels)? _composite;
|
||||
private CanvasBackground _background = CanvasBackground.Default;
|
||||
|
||||
/// <param name="isBlank">Leeg canvas: volledig transparant laten in plaats van als alpha-loze afbeelding te behandelen.</param>
|
||||
public EditorDocument(BitmapSource source, bool isBlank = false)
|
||||
{
|
||||
SetPixels(PixelUtil.ToOpaqueBgra(source), source.PixelWidth, source.PixelHeight);
|
||||
byte[] pixels = isBlank || !PixelUtil.IsFullyTransparent(source)
|
||||
? PixelUtil.ToBgra(source)
|
||||
: PixelUtil.ToOpaqueBgra(source);
|
||||
SetPixels(pixels, source.PixelWidth, source.PixelHeight);
|
||||
}
|
||||
|
||||
public BitmapSource Image { get; private set; } = null!;
|
||||
@@ -26,21 +34,70 @@ public sealed class EditorDocument
|
||||
|
||||
public int Height => Image.PixelHeight;
|
||||
|
||||
/// <summary>Of ergens doorheen te kijken is; alleen dan heeft een achtergrond zin.</summary>
|
||||
public bool HasTransparency { get; private set; }
|
||||
|
||||
public List<Annotation> Items { get; private set; } = new();
|
||||
|
||||
public CanvasBackground Background
|
||||
{
|
||||
get => _background;
|
||||
set
|
||||
{
|
||||
_background = value;
|
||||
_composite = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Achtergrond plus knipsel zonder annotaties: de bron voor vervagen en pixeleren.</summary>
|
||||
public BitmapSource EffectImage => Composite().Image;
|
||||
|
||||
public byte[] EffectPixels => Composite().Pixels;
|
||||
|
||||
private void SetPixels(byte[] pixels, int width, int height)
|
||||
{
|
||||
Image = PixelUtil.CreateBitmap(pixels, width, height);
|
||||
Pixels = pixels;
|
||||
HasTransparency = PixelUtil.HasTransparency(pixels);
|
||||
_composite = null;
|
||||
}
|
||||
|
||||
public DocumentState CaptureState() => new(Image, Pixels, Items.Select(i => i.Clone()).ToList());
|
||||
private (BitmapSource Image, byte[] Pixels) Composite()
|
||||
{
|
||||
if (_composite is { } cached)
|
||||
{
|
||||
return cached;
|
||||
}
|
||||
|
||||
if (!HasTransparency || !Background.IsVisible)
|
||||
{
|
||||
_composite = (Image, Pixels);
|
||||
return _composite.Value;
|
||||
}
|
||||
|
||||
var visual = new DrawingVisual();
|
||||
using (DrawingContext dc = visual.RenderOpen())
|
||||
{
|
||||
Background.Render(dc, Width, Height);
|
||||
dc.DrawImage(Image, new Rect(0, 0, Width, Height));
|
||||
}
|
||||
|
||||
var bitmap = new RenderTargetBitmap(Width, Height, 96, 96, PixelFormats.Pbgra32);
|
||||
bitmap.Render(visual);
|
||||
byte[] pixels = PixelUtil.ToBgra(bitmap);
|
||||
_composite = (PixelUtil.CreateBitmap(pixels, Width, Height), pixels);
|
||||
return _composite.Value;
|
||||
}
|
||||
|
||||
public DocumentState CaptureState() => new(Image, Pixels, Items.Select(i => i.Clone()).ToList(), Background);
|
||||
|
||||
public void Restore(DocumentState state)
|
||||
{
|
||||
Image = state.Image;
|
||||
Pixels = state.Pixels;
|
||||
HasTransparency = PixelUtil.HasTransparency(state.Pixels);
|
||||
Items = state.Items.Select(i => i.Clone()).ToList();
|
||||
Background = state.Background;
|
||||
}
|
||||
|
||||
public void Crop(Int32Rect area)
|
||||
@@ -62,7 +119,7 @@ public sealed class EditorDocument
|
||||
/// <summary>Vervangt het knipsel door de platgemaakte versie; de annotaties zitten daarna in de pixels.</summary>
|
||||
public void Flatten(BitmapSource rendered)
|
||||
{
|
||||
SetPixels(PixelUtil.ToOpaqueBgra(rendered), rendered.PixelWidth, rendered.PixelHeight);
|
||||
SetPixels(PixelUtil.ToBgra(rendered), rendered.PixelWidth, rendered.PixelHeight);
|
||||
Items = new List<Annotation>();
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:local="clr-namespace:Kniip.Editor"
|
||||
xmlns:sys="clr-namespace:System;assembly=mscorlib"
|
||||
Title="kniip" Width="1180" Height="780" MinWidth="760" MinHeight="420"
|
||||
WindowStartupLocation="CenterScreen" Background="{DynamicResource WindowBrush}" Foreground="{DynamicResource InkBrush}"
|
||||
FontFamily="Segoe UI" FontSize="13" UseLayoutRounding="True"
|
||||
@@ -10,6 +11,24 @@
|
||||
<Window.Resources>
|
||||
<local:CornerPreviewConverter x:Key="CornerPreview" />
|
||||
<local:CursorIconConverter x:Key="CursorIcon" />
|
||||
<local:ShapeIconConverter x:Key="ShapeIcon" />
|
||||
|
||||
<Style x:Key="ArrangeButton" TargetType="Button" BasedOn="{StaticResource FlatButton}">
|
||||
<Setter Property="MinWidth" Value="30" />
|
||||
<Setter Property="MinHeight" Value="30" />
|
||||
<Setter Property="Padding" Value="4" />
|
||||
<Setter Property="Margin" Value="0" />
|
||||
</Style>
|
||||
<Style x:Key="ArrangeAccent" TargetType="Path" BasedOn="{StaticResource IconPath}">
|
||||
<Setter Property="Stroke" Value="{DynamicResource AccentBrush}" />
|
||||
<Setter Property="StrokeThickness" Value="1.5" />
|
||||
</Style>
|
||||
<Style x:Key="ArrangeDivider" TargetType="Rectangle">
|
||||
<Setter Property="Width" Value="1" />
|
||||
<Setter Property="Height" Value="20" />
|
||||
<Setter Property="Margin" Value="4,0" />
|
||||
<Setter Property="Fill" Value="{DynamicResource DividerBrush}" />
|
||||
</Style>
|
||||
|
||||
<!-- Dikte: stip die meegroeit met de lijndikte -->
|
||||
<DataTemplate x:Key="ThicknessTemplate">
|
||||
@@ -148,6 +167,93 @@
|
||||
</DataTemplate.Triggers>
|
||||
</DataTemplate>
|
||||
|
||||
<!-- Vorm van een tekstobject: kader, ballon, ovaal of wolk -->
|
||||
<DataTemplate x:Key="TextShapeTemplate">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Path x:Name="Icon" Data="M2.5,3.5 H17.5 V14.5 H2.5 Z" Stroke="{DynamicResource InkBrush}" StrokeThickness="1.3" StrokeLineJoin="Round"
|
||||
Width="20" Height="18" VerticalAlignment="Center" />
|
||||
<TextBlock x:Name="Label" Text="Kader" Margin="8,0,0,0" VerticalAlignment="Center" FontSize="12" />
|
||||
</StackPanel>
|
||||
<DataTemplate.Triggers>
|
||||
<DataTrigger Binding="{Binding}" Value="{x:Static local:TextShape.Balloon}">
|
||||
<Setter TargetName="Icon" Property="Data" Value="M4,2.5 H16 Q17.5,2.5 17.5,4 V11 Q17.5,12.5 16,12.5 H9 L4.5,16.5 L6,12.5 H4 Q2.5,12.5 2.5,11 V4 Q2.5,2.5 4,2.5 Z" />
|
||||
<Setter TargetName="Label" Property="Text" Value="Ballon" />
|
||||
</DataTrigger>
|
||||
<DataTrigger Binding="{Binding}" Value="{x:Static local:TextShape.Oval}">
|
||||
<Setter TargetName="Icon" Property="Data" Value="M10,2 C14.7,2 18,4.5 18,7.5 C18,10.5 14.7,13 10,13 C9,13 8.1,12.9 7.3,12.7 L3.8,16.5 L4.8,12 C3,11 2,9.4 2,7.5 C2,4.5 5.3,2 10,2 Z" />
|
||||
<Setter TargetName="Label" Property="Text" Value="Ovale ballon" />
|
||||
</DataTrigger>
|
||||
<DataTrigger Binding="{Binding}" Value="{x:Static local:TextShape.Cloud}">
|
||||
<Setter TargetName="Icon" Property="Data" Value="M6.5,12 A2.8,2.8 0 0 1 4.2,6.8 A3.3,3.3 0 0 1 9.2,3.6 A3.3,3.3 0 0 1 15,4.8 A2.8,2.8 0 0 1 16.2,10.5 A2.8,2.8 0 0 1 12,12.2 A3,3 0 0 1 6.5,12 Z M3.8,14.6 a1.2,1.2 0 1 0 2.4,0 a1.2,1.2 0 1 0 -2.4,0 Z M1.8,17 a0.7,0.7 0 1 0 1.4,0 a0.7,0.7 0 1 0 -1.4,0 Z" />
|
||||
<Setter TargetName="Label" Property="Text" Value="Wolk" />
|
||||
</DataTrigger>
|
||||
<DataTrigger Binding="{Binding RelativeSource={RelativeSource AncestorType=ComboBoxItem}}" Value="{x:Null}">
|
||||
<Setter TargetName="Label" Property="Visibility" Value="Collapsed" />
|
||||
</DataTrigger>
|
||||
</DataTemplate.Triggers>
|
||||
</DataTemplate>
|
||||
|
||||
<!-- Vorm van het vormengereedschap -->
|
||||
<DataTemplate x:Key="ShapeKindTemplate">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Path Data="{Binding Converter={StaticResource ShapeIcon}}" Stroke="{DynamicResource InkBrush}" StrokeThickness="1.2"
|
||||
StrokeLineJoin="Round" Stretch="Uniform" Width="16" Height="16" Margin="2,1" VerticalAlignment="Center" />
|
||||
<TextBlock x:Name="Label" Text="Driehoek" Margin="8,0,0,0" VerticalAlignment="Center" FontSize="12" />
|
||||
</StackPanel>
|
||||
<DataTemplate.Triggers>
|
||||
<DataTrigger Binding="{Binding}" Value="{x:Static local:ShapeKind.BlockArrow}">
|
||||
<Setter TargetName="Label" Property="Text" Value="Pijl" />
|
||||
</DataTrigger>
|
||||
<DataTrigger Binding="{Binding}" Value="{x:Static local:ShapeKind.Star}">
|
||||
<Setter TargetName="Label" Property="Text" Value="Ster" />
|
||||
</DataTrigger>
|
||||
<DataTrigger Binding="{Binding}" Value="{x:Static local:ShapeKind.Cloud}">
|
||||
<Setter TargetName="Label" Property="Text" Value="Wolk" />
|
||||
</DataTrigger>
|
||||
<DataTrigger Binding="{Binding}" Value="{x:Static local:ShapeKind.Balloon}">
|
||||
<Setter TargetName="Label" Property="Text" Value="Ballon" />
|
||||
</DataTrigger>
|
||||
<DataTrigger Binding="{Binding}" Value="{x:Static local:ShapeKind.Heart}">
|
||||
<Setter TargetName="Label" Property="Text" Value="Hart" />
|
||||
</DataTrigger>
|
||||
<DataTrigger Binding="{Binding}" Value="{x:Static local:ShapeKind.Moon}">
|
||||
<Setter TargetName="Label" Property="Text" Value="Maan" />
|
||||
</DataTrigger>
|
||||
<DataTrigger Binding="{Binding}" Value="{x:Static local:ShapeKind.Drop}">
|
||||
<Setter TargetName="Label" Property="Text" Value="Druppel" />
|
||||
</DataTrigger>
|
||||
<DataTrigger Binding="{Binding}" Value="{x:Static local:ShapeKind.Egg}">
|
||||
<Setter TargetName="Label" Property="Text" Value="Ei" />
|
||||
</DataTrigger>
|
||||
<DataTrigger Binding="{Binding}" Value="{x:Static local:ShapeKind.Lightning}">
|
||||
<Setter TargetName="Label" Property="Text" Value="Bliksemschicht" />
|
||||
</DataTrigger>
|
||||
<DataTrigger Binding="{Binding}" Value="{x:Static local:ShapeKind.Spiral}">
|
||||
<Setter TargetName="Label" Property="Text" Value="Spiraal" />
|
||||
</DataTrigger>
|
||||
<DataTrigger Binding="{Binding RelativeSource={RelativeSource AncestorType=ComboBoxItem}}" Value="{x:Null}">
|
||||
<Setter TargetName="Label" Property="Visibility" Value="Collapsed" />
|
||||
</DataTrigger>
|
||||
</DataTemplate.Triggers>
|
||||
</DataTemplate>
|
||||
|
||||
<!-- Aantal rondjes in de staart van een wolk; 0 = automatisch -->
|
||||
<DataTemplate x:Key="CloudBubblesTemplate">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Path Data="M13.5,4.5 a3,3 0 1 0 6,0 a3,3 0 1 0 -6,0 Z M7.5,10 a2,2 0 1 0 4,0 a2,2 0 1 0 -4,0 Z M2.5,15 a1.2,1.2 0 1 0 2.4,0 a1.2,1.2 0 1 0 -2.4,0 Z"
|
||||
Stroke="{DynamicResource InkBrush}" StrokeThickness="1.2" Width="20" Height="18" VerticalAlignment="Center" />
|
||||
<TextBlock x:Name="Label" Text="{Binding}" FontSize="11" Margin="4,0,0,0" VerticalAlignment="Center" Foreground="{DynamicResource SubtleTextBrush}" />
|
||||
</StackPanel>
|
||||
<DataTemplate.Triggers>
|
||||
<DataTrigger Binding="{Binding}">
|
||||
<DataTrigger.Value>
|
||||
<sys:Int32>0</sys:Int32>
|
||||
</DataTrigger.Value>
|
||||
<Setter TargetName="Label" Property="Text" Value="Auto" />
|
||||
</DataTrigger>
|
||||
</DataTemplate.Triggers>
|
||||
</DataTemplate>
|
||||
|
||||
<!-- Effect: pixelblokjes of zachte vlek -->
|
||||
<DataTemplate x:Key="BlurModeTemplate">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
@@ -177,6 +283,49 @@
|
||||
</DataTrigger>
|
||||
</DataTemplate.Triggers>
|
||||
</DataTemplate>
|
||||
|
||||
<DrawingBrush x:Key="CheckerIcon" TileMode="Tile" Viewport="0,0,8,8" ViewportUnits="Absolute">
|
||||
<DrawingBrush.Drawing>
|
||||
<DrawingGroup>
|
||||
<GeometryDrawing Brush="White" Geometry="M0,0 H2 V2 H0 Z" />
|
||||
<GeometryDrawing Brush="#E4E4E4" Geometry="M0,0 H1 V1 H0 Z M1,1 H2 V2 H1 Z" />
|
||||
</DrawingGroup>
|
||||
</DrawingBrush.Drawing>
|
||||
</DrawingBrush>
|
||||
|
||||
<!-- Achtergrond van het canvas: geen, kleur, verloop of afbeelding -->
|
||||
<DataTemplate x:Key="BackgroundKindTemplate">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Grid Width="16" Height="16" VerticalAlignment="Center">
|
||||
<Border x:Name="Swatch" CornerRadius="3" BorderBrush="{DynamicResource ControlStrongBorderBrush}" BorderThickness="1"
|
||||
Background="{StaticResource CheckerIcon}" />
|
||||
<TextBlock x:Name="ImageIcon" Text="" Style="{StaticResource IconText}" FontSize="15" Visibility="Collapsed" />
|
||||
</Grid>
|
||||
<TextBlock x:Name="Label" Text="Geen (transparant)" Margin="8,0,0,0" VerticalAlignment="Center" FontSize="12" />
|
||||
</StackPanel>
|
||||
<DataTemplate.Triggers>
|
||||
<DataTrigger Binding="{Binding}" Value="{x:Static local:BackgroundKind.Color}">
|
||||
<Setter TargetName="Swatch" Property="Background" Value="{DynamicResource AccentBrush}" />
|
||||
<Setter TargetName="Label" Property="Text" Value="Kleur" />
|
||||
</DataTrigger>
|
||||
<DataTrigger Binding="{Binding}" Value="{x:Static local:BackgroundKind.Gradient}">
|
||||
<Setter TargetName="Swatch" Property="Background">
|
||||
<Setter.Value>
|
||||
<LinearGradientBrush StartPoint="0,0" EndPoint="1,1">
|
||||
<GradientStop Color="White" Offset="0" />
|
||||
<GradientStop Color="#0078D4" Offset="1" />
|
||||
</LinearGradientBrush>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
<Setter TargetName="Label" Property="Text" Value="Verloop" />
|
||||
</DataTrigger>
|
||||
<DataTrigger Binding="{Binding}" Value="{x:Static local:BackgroundKind.Image}">
|
||||
<Setter TargetName="Swatch" Property="Visibility" Value="Collapsed" />
|
||||
<Setter TargetName="ImageIcon" Property="Visibility" Value="Visible" />
|
||||
<Setter TargetName="Label" Property="Text" Value="Afbeelding" />
|
||||
</DataTrigger>
|
||||
</DataTemplate.Triggers>
|
||||
</DataTemplate>
|
||||
</Window.Resources>
|
||||
<DockPanel>
|
||||
<!-- Statusbalk -->
|
||||
@@ -222,6 +371,10 @@
|
||||
</Path.Data>
|
||||
</Path>
|
||||
</RadioButton>
|
||||
<RadioButton GroupName="Tools" Tag="Shape" local:ToolbarPanel.ItemId="Shape" local:ToolbarPanel.Title="Vormen"
|
||||
Style="{StaticResource ToolToggle}" ToolTip="Vormen (F) - Shift voor gelijke breedte en hoogte" Checked="Tool_Checked">
|
||||
<Path x:Name="ShapeToolIcon" Style="{StaticResource IconPath}" Stretch="Uniform" Width="15" Height="15" Margin="2.5" />
|
||||
</RadioButton>
|
||||
<RadioButton GroupName="Tools" Tag="Line" local:ToolbarPanel.ItemId="Line" local:ToolbarPanel.Title="Lijn"
|
||||
Style="{StaticResource ToolToggle}" ToolTip="Lijn (L) - Shift voor hoeken van 45°" Checked="Tool_Checked">
|
||||
<Path Style="{StaticResource IconPath}" Data="M4,16 L16,4" />
|
||||
@@ -287,6 +440,15 @@
|
||||
<TextBlock x:Name="ScreencastLabel" Text="Screencast" FontSize="12" VerticalAlignment="Center" Margin="6,0,0,0" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
<Button x:Name="OcrButton" local:ToolbarPanel.ItemId="OcrSnip" local:ToolbarPanel.Title="OCR"
|
||||
Style="{StaticResource ProminentButton}" Click="Ocr_Click"
|
||||
ToolTip="OCR: sleep een rechthoek op het scherm en kopieer de tekst daarin. Ctrl+Shift+T leest de tekst van dit knipsel.">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Path Style="{StaticResource IconPath}" StrokeThickness="1.2"
|
||||
Data="M3,7 V5.5 A2.5,2.5 0 0 1 5.5,3 H7 M13,3 H14.5 A2.5,2.5 0 0 1 17,5.5 V7 M17,13 V14.5 A2.5,2.5 0 0 1 14.5,17 H13 M7,17 H5.5 A2.5,2.5 0 0 1 3,14.5 V13 M7,7.5 H13 M7,10 H13 M7,12.5 H10.5" />
|
||||
<TextBlock x:Name="OcrLabel" Text="OCR" FontSize="12" VerticalAlignment="Center" Margin="6,0,0,0" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
|
||||
<!-- Wat tussen de twee spacers staat, komt in het midden van de werkbalk -->
|
||||
<local:ToolbarSpacer local:ToolbarPanel.ItemId="Spacer2" />
|
||||
@@ -312,6 +474,12 @@
|
||||
<TextBlock Text="" Style="{StaticResource IconText}" />
|
||||
</Button>
|
||||
<local:ToolbarSeparator local:ToolbarPanel.ItemId="Sep4" />
|
||||
<ToggleButton x:Name="RulersToggle" local:ToolbarPanel.ItemId="Rulers" local:ToolbarPanel.Title="Linialen"
|
||||
Style="{StaticResource ToolToggle}" ToolTip="Linialen en hulplijnen (Ctrl+R) - sleep vanuit een liniaal voor een hulplijn"
|
||||
Click="Rulers_Click">
|
||||
<Path Style="{StaticResource IconPath}"
|
||||
Data="M2.5,6.5 H17.5 V13.5 H2.5 Z M5,6.5 V9 M7.5,6.5 V10.5 M10,6.5 V9 M12.5,6.5 V10.5 M15,6.5 V9" />
|
||||
</ToggleButton>
|
||||
<Button local:ToolbarPanel.ItemId="ZoomOut" local:ToolbarPanel.Title="Uitzoomen"
|
||||
Style="{StaticResource FlatButton}" ToolTip="Uitzoomen (Ctrl+-)" Click="ZoomOut_Click">
|
||||
<TextBlock Text="" Style="{StaticResource IconText}" />
|
||||
@@ -325,6 +493,14 @@
|
||||
<TextBlock Text="" Style="{StaticResource IconText}" />
|
||||
</Button>
|
||||
<local:ToolbarSeparator local:ToolbarPanel.ItemId="Sep5" />
|
||||
<Button x:Name="FoldersButton" local:ToolbarPanel.ItemId="Folders" local:ToolbarPanel.Title="Mappen openen"
|
||||
Style="{StaticResource FlatButton}" ToolTip="Screenshot- of screencastmap openen" Click="Folders_Click">
|
||||
<TextBlock Text="" Style="{StaticResource IconText}" />
|
||||
</Button>
|
||||
<Button local:ToolbarPanel.ItemId="Print" local:ToolbarPanel.Title="Printen"
|
||||
Style="{StaticResource FlatButton}" ToolTip="Printen (Ctrl+P)" Click="Print_Click">
|
||||
<TextBlock Text="" Style="{StaticResource IconText}" />
|
||||
</Button>
|
||||
<Button x:Name="SettingsButton" local:ToolbarPanel.ItemId="Settings" local:ToolbarPanel.Title="Instellingen"
|
||||
Style="{StaticResource FlatButton}" ToolTip="Instellingen" Click="Settings_Click">
|
||||
<Grid>
|
||||
@@ -335,10 +511,13 @@
|
||||
</Grid>
|
||||
</Button>
|
||||
<Button x:Name="SaveButton" local:ToolbarPanel.ItemId="Save" local:ToolbarPanel.Title="Opslaan"
|
||||
Style="{StaticResource SecondaryButton}" ToolTip="Opslaan (Ctrl+S); Shift+klik of Ctrl+Shift+S = opslaan als" Click="Save_Click">
|
||||
Style="{StaticResource SecondaryButton}" ToolTip="Opslaan (Ctrl+S); Shift+klik of Ctrl+Shift+S = opslaan als; het pijltje toont meer opties" Click="Save_Click">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Text="" Style="{StaticResource IconText}" FontSize="14" />
|
||||
<TextBlock x:Name="SaveLabel" Text="Opslaan" Margin="6,0,0,0" />
|
||||
<TextBlock x:Name="SaveMenuChevron" Text="" Style="{StaticResource IconText}" FontSize="9"
|
||||
Background="Transparent" Padding="4,2,0,2" Margin="2,0,0,0"
|
||||
ToolTip="Meer opslagopties" PreviewMouseLeftButtonDown="SaveMenu_MouseDown" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
<Button x:Name="UploadButton" local:ToolbarPanel.ItemId="Upload" local:ToolbarPanel.Title="Uploaden"
|
||||
@@ -366,6 +545,16 @@
|
||||
<Border x:Name="PropsBar" DockPanel.Dock="Top" Background="{DynamicResource CardBrush}" BorderBrush="{DynamicResource DividerBrush}" BorderThickness="0,0,0,1"
|
||||
Padding="12,4" MinHeight="42">
|
||||
<WrapPanel x:Name="PropsPanel" VerticalAlignment="Center" HorizontalAlignment="Center">
|
||||
<StackPanel x:Name="ShapeGroup" Style="{StaticResource PropGroup}">
|
||||
<ComboBox x:Name="ShapeKindCombo" Style="{StaticResource MiniCombo}" ItemTemplate="{StaticResource ShapeKindTemplate}"
|
||||
ToolTip="Vorm" SelectionChanged="ShapeKind_Changed" />
|
||||
</StackPanel>
|
||||
<StackPanel x:Name="TextShapeGroup" Style="{StaticResource PropGroup}">
|
||||
<ComboBox x:Name="TextShapeCombo" Style="{StaticResource MiniCombo}" ItemTemplate="{StaticResource TextShapeTemplate}"
|
||||
ToolTip="Vorm" SelectionChanged="TextShape_Changed" />
|
||||
<ComboBox x:Name="CloudBubblesCombo" Style="{StaticResource MiniCombo}" ItemTemplate="{StaticResource CloudBubblesTemplate}"
|
||||
ToolTip="Aantal bolletjes naar de punt" SelectionChanged="CloudBubbles_Changed" />
|
||||
</StackPanel>
|
||||
<StackPanel x:Name="StrokeGroup" Style="{StaticResource PropGroup}">
|
||||
<local:ColorPickerButton x:Name="StrokePicker" ShowAsOutline="True" ToolTip="Randkleur" />
|
||||
</StackPanel>
|
||||
@@ -436,28 +625,199 @@
|
||||
</Grid>
|
||||
</ToggleButton>
|
||||
</StackPanel>
|
||||
<StackPanel x:Name="ArrangeGroup" Style="{StaticResource PropGroup}">
|
||||
<Rectangle Style="{StaticResource ArrangeDivider}" />
|
||||
<ToggleButton x:Name="ArrangeToggle" Style="{StaticResource MiniToggle}" ToolTip="Uitlijnen, spiegelen en volgorde"
|
||||
Checked="ArrangeToggle_Changed" Unchecked="ArrangeToggle_Changed">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Grid>
|
||||
<Path Style="{StaticResource IconPath}" Data="M5.5,5 H15 V8.5 H5.5 Z M5.5,11.5 H11.5 V15 H5.5 Z" />
|
||||
<Path Style="{StaticResource ArrangeAccent}" Data="M3,2.5 V17.5" />
|
||||
</Grid>
|
||||
<TextBlock x:Name="ArrangeChevron" Text="" Style="{StaticResource IconText}" FontSize="10" Margin="4,0,0,0" />
|
||||
</StackPanel>
|
||||
</ToggleButton>
|
||||
<StackPanel x:Name="ArrangeButtons" Orientation="Horizontal" Visibility="Collapsed" Margin="4,0,0,0">
|
||||
<Button Style="{StaticResource ArrangeButton}" ToolTip="Links uitlijnen op de afbeelding" Click="AlignLeft_Click">
|
||||
<Grid>
|
||||
<Path Style="{StaticResource IconPath}" Data="M5.5,5 H15 V8.5 H5.5 Z M5.5,11.5 H11.5 V15 H5.5 Z" />
|
||||
<Path Style="{StaticResource ArrangeAccent}" Data="M3,2.5 V17.5" />
|
||||
</Grid>
|
||||
</Button>
|
||||
<Button Style="{StaticResource ArrangeButton}" ToolTip="Horizontaal centreren op de afbeelding" Click="AlignCenter_Click">
|
||||
<Grid>
|
||||
<Path Style="{StaticResource IconPath}" Data="M4.5,5 H15.5 V8.5 H4.5 Z M6.5,11.5 H13.5 V15 H6.5 Z" />
|
||||
<Path Style="{StaticResource ArrangeAccent}" Data="M10,2.5 V17.5" />
|
||||
</Grid>
|
||||
</Button>
|
||||
<Button Style="{StaticResource ArrangeButton}" ToolTip="Rechts uitlijnen op de afbeelding" Click="AlignRight_Click">
|
||||
<Grid>
|
||||
<Path Style="{StaticResource IconPath}" Data="M5,5 H14.5 V8.5 H5 Z M8.5,11.5 H14.5 V15 H8.5 Z" />
|
||||
<Path Style="{StaticResource ArrangeAccent}" Data="M17,2.5 V17.5" />
|
||||
</Grid>
|
||||
</Button>
|
||||
<Button Style="{StaticResource ArrangeButton}" ToolTip="Boven uitlijnen op de afbeelding" Click="AlignTop_Click">
|
||||
<Grid>
|
||||
<Path Style="{StaticResource IconPath}" Data="M5,5.5 H8.5 V15 H5 Z M11.5,5.5 H15 V11.5 H11.5 Z" />
|
||||
<Path Style="{StaticResource ArrangeAccent}" Data="M2.5,3 H17.5" />
|
||||
</Grid>
|
||||
</Button>
|
||||
<Button Style="{StaticResource ArrangeButton}" ToolTip="Verticaal centreren op de afbeelding" Click="AlignMiddle_Click">
|
||||
<Grid>
|
||||
<Path Style="{StaticResource IconPath}" Data="M5,4.5 H8.5 V15.5 H5 Z M11.5,6.5 H15 V13.5 H11.5 Z" />
|
||||
<Path Style="{StaticResource ArrangeAccent}" Data="M2.5,10 H17.5" />
|
||||
</Grid>
|
||||
</Button>
|
||||
<Button Style="{StaticResource ArrangeButton}" ToolTip="Onder uitlijnen op de afbeelding" Click="AlignBottom_Click">
|
||||
<Grid>
|
||||
<Path Style="{StaticResource IconPath}" Data="M5,5 H8.5 V14.5 H5 Z M11.5,8.5 H15 V14.5 H11.5 Z" />
|
||||
<Path Style="{StaticResource ArrangeAccent}" Data="M2.5,17 H17.5" />
|
||||
</Grid>
|
||||
</Button>
|
||||
<Rectangle Style="{StaticResource ArrangeDivider}" />
|
||||
<Button x:Name="FlipHButton" Style="{StaticResource ArrangeButton}" ToolTip="Horizontaal spiegelen" Click="FlipH_Click">
|
||||
<Grid>
|
||||
<Path Style="{StaticResource IconPath}" Fill="{DynamicResource InkBrush}" Data="M8,5 L3,15 H8 Z" />
|
||||
<Path Style="{StaticResource IconPath}" Data="M12,5 L17,15 H12 Z" />
|
||||
<Path Style="{StaticResource ArrangeAccent}" Data="M10,2.5 V17.5" />
|
||||
</Grid>
|
||||
</Button>
|
||||
<Button x:Name="FlipVButton" Style="{StaticResource ArrangeButton}" ToolTip="Verticaal spiegelen" Click="FlipV_Click">
|
||||
<Grid>
|
||||
<Path Style="{StaticResource IconPath}" Fill="{DynamicResource InkBrush}" Data="M5,8 L15,3 V8 Z" />
|
||||
<Path Style="{StaticResource IconPath}" Data="M5,12 L15,17 V12 Z" />
|
||||
<Path Style="{StaticResource ArrangeAccent}" Data="M2.5,10 H17.5" />
|
||||
</Grid>
|
||||
</Button>
|
||||
<Button x:Name="RotateLeftButton" Style="{StaticResource ArrangeButton}" ToolTip="90° linksom draaien" Click="RotateLeft_Click">
|
||||
<Grid>
|
||||
<Path Style="{StaticResource IconPath}" Data="M8.5,8.5 H15.5 V15.5 H8.5 Z" />
|
||||
<Path Style="{StaticResource ArrangeAccent}" Data="M3.5,11 A6.5,6.5 0 0 1 10,4.5 M3.5,11 L1.8,8.6 M3.5,11 L5.9,9.4" />
|
||||
</Grid>
|
||||
</Button>
|
||||
<Button x:Name="RotateRightButton" Style="{StaticResource ArrangeButton}" ToolTip="90° rechtsom draaien (sleep de ronde greep boven het object om vrij te draaien)" Click="RotateRight_Click">
|
||||
<Grid>
|
||||
<Path Style="{StaticResource IconPath}" Data="M4.5,8.5 H11.5 V15.5 H4.5 Z" />
|
||||
<Path Style="{StaticResource ArrangeAccent}" Data="M16.5,11 A6.5,6.5 0 0 0 10,4.5 M16.5,11 L18.2,8.6 M16.5,11 L14.1,9.4" />
|
||||
</Grid>
|
||||
</Button>
|
||||
<Rectangle Style="{StaticResource ArrangeDivider}" />
|
||||
<Button x:Name="ForwardButton" Style="{StaticResource ArrangeButton}" ToolTip="Naar voren (PageUp)" Click="BringForward_Click">
|
||||
<TextBlock Text="" Style="{StaticResource IconText}" />
|
||||
</Button>
|
||||
<Button x:Name="BackwardButton" Style="{StaticResource ArrangeButton}" ToolTip="Naar achteren (PageDown)" Click="SendBackward_Click">
|
||||
<TextBlock Text="" Style="{StaticResource IconText}" />
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
<TextBlock x:Name="NoPropsText" Style="{StaticResource PropLabel}" VerticalAlignment="Center" />
|
||||
<StackPanel x:Name="BackgroundGroup" Style="{StaticResource PropGroup}">
|
||||
<Rectangle Style="{StaticResource ArrangeDivider}" />
|
||||
<TextBlock Text="Achtergrond" Style="{StaticResource PropLabel}" Margin="4,0,4,0" />
|
||||
<ComboBox x:Name="BackgroundKindCombo" Style="{StaticResource MiniCombo}" ItemTemplate="{StaticResource BackgroundKindTemplate}"
|
||||
ToolTip="Achtergrond onder de transparante delen" SelectionChanged="BackgroundKind_Changed" />
|
||||
<local:ColorPickerButton x:Name="BackgroundColorPicker" ToolTip="Kleur" />
|
||||
<local:ColorPickerButton x:Name="BackgroundColor2Picker" ToolTip="Tweede kleur van het verloop" />
|
||||
<local:GradientAngleDial x:Name="GradientDial" Margin="2,0" />
|
||||
<Button x:Name="BackgroundImageButton" Style="{StaticResource FlatButton}" Padding="6,4" MinWidth="0" MinHeight="30" Margin="2,0"
|
||||
BorderBrush="{DynamicResource ControlBorderBrush}" Background="{DynamicResource ControlBrush}"
|
||||
ToolTip="Afbeelding kiezen; die vult het hele canvas" Click="BackgroundImage_Click">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Grid Width="20" Height="20">
|
||||
<TextBlock x:Name="BackgroundImageIcon" Text="" Style="{StaticResource IconText}" />
|
||||
<Image x:Name="BackgroundImageThumb" Stretch="UniformToFill" Visibility="Collapsed">
|
||||
<Image.Clip>
|
||||
<RectangleGeometry Rect="0,0,20,20" RadiusX="3" RadiusY="3" />
|
||||
</Image.Clip>
|
||||
</Image>
|
||||
</Grid>
|
||||
<TextBlock Text="Kiezen..." FontSize="12" VerticalAlignment="Center" Margin="6,0,0,0" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</WrapPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Canvas -->
|
||||
<ScrollViewer x:Name="Scroller" HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto"
|
||||
Background="{DynamicResource CanvasBrush}" PreviewMouseWheel="Scroller_PreviewMouseWheel" Focusable="False">
|
||||
<Grid HorizontalAlignment="Center" VerticalAlignment="Center" Margin="24">
|
||||
<Border BorderBrush="{DynamicResource CanvasBorderBrush}" BorderThickness="1" Margin="-1" SnapsToDevicePixels="True" />
|
||||
<Grid x:Name="CanvasHost">
|
||||
<Grid.LayoutTransform>
|
||||
<ScaleTransform x:Name="ZoomTransform" />
|
||||
</Grid.LayoutTransform>
|
||||
<local:AnnotationSurface x:Name="Surface"
|
||||
MouseLeftButtonDown="Surface_MouseLeftButtonDown"
|
||||
MouseMove="Surface_MouseMove"
|
||||
MouseLeftButtonUp="Surface_MouseLeftButtonUp"
|
||||
MouseRightButtonUp="Surface_MouseRightButtonUp"
|
||||
LostMouseCapture="Surface_LostMouseCapture" />
|
||||
<Canvas x:Name="TextLayer" />
|
||||
<!-- Extra balk in de drie-split-indeling, aan een kant van het canvas; de code verplaatst Nieuw en Screencast hierheen -->
|
||||
<Border x:Name="CaptureBar" DockPanel.Dock="Left" Background="{DynamicResource ToolbarBrush}" BorderBrush="{DynamicResource DividerBrush}"
|
||||
BorderThickness="0,0,1,0" Padding="6,8" Visibility="Collapsed"
|
||||
SizeChanged="Toolbar_SizeChanged" ContextMenuOpening="Toolbar_ContextMenuOpening">
|
||||
<Border.ContextMenu>
|
||||
<ContextMenu x:Name="CaptureBarMenu" />
|
||||
</Border.ContextMenu>
|
||||
<local:ToolbarPanel x:Name="CaptureItems" Orientation="Vertical" OrderChanged="ToolbarItems_OrderChanged" />
|
||||
</Border>
|
||||
|
||||
<!-- Canvas met linialen; sleep vanuit een liniaal om een hulplijn te maken -->
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Border x:Name="RulerCorner" Width="20" Height="20" Background="{DynamicResource ToolbarBrush}"
|
||||
BorderBrush="{DynamicResource DividerBrush}" BorderThickness="0,0,1,1"
|
||||
MouseRightButtonUp="RulerCorner_MouseRightButtonUp">
|
||||
<Button x:Name="RulerFolderButton" Style="{StaticResource FlatButton}" MinWidth="0" MinHeight="0" Margin="0"
|
||||
Padding="0" BorderThickness="0" ToolTip="Map openen (rechtsklik: hulplijnen wissen)" Click="RulerFolder_Click">
|
||||
<TextBlock Text="" Style="{StaticResource IconText}" FontSize="12" />
|
||||
</Button>
|
||||
</Border>
|
||||
<local:RulerControl x:Name="HRuler" Grid.Column="1" Height="20" Orientation="Horizontal"
|
||||
Background="{DynamicResource ToolbarBrush}" ImageBackground="{DynamicResource CardBrush}"
|
||||
Foreground="{DynamicResource SubtleTextBrush}" BorderBrush="{DynamicResource DividerBrush}"
|
||||
Cursor="SizeNS" MouseLeftButtonDown="Ruler_MouseLeftButtonDown" MouseMove="Ruler_MouseMove"
|
||||
MouseLeftButtonUp="Ruler_MouseLeftButtonUp" LostMouseCapture="Ruler_LostMouseCapture" />
|
||||
<local:RulerControl x:Name="VRuler" Grid.Row="1" Width="20" Orientation="Vertical"
|
||||
Background="{DynamicResource ToolbarBrush}" ImageBackground="{DynamicResource CardBrush}"
|
||||
Foreground="{DynamicResource SubtleTextBrush}" BorderBrush="{DynamicResource DividerBrush}"
|
||||
Cursor="SizeWE" MouseLeftButtonDown="Ruler_MouseLeftButtonDown" MouseMove="Ruler_MouseMove"
|
||||
MouseLeftButtonUp="Ruler_MouseLeftButtonUp" LostMouseCapture="Ruler_LostMouseCapture" />
|
||||
<ScrollViewer x:Name="Scroller" Grid.Row="1" Grid.Column="1" HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto"
|
||||
Background="{DynamicResource CanvasBrush}" PreviewMouseWheel="Scroller_PreviewMouseWheel" Focusable="False">
|
||||
<Grid HorizontalAlignment="Center" VerticalAlignment="Center" Margin="24">
|
||||
<Border BorderBrush="{DynamicResource CanvasBorderBrush}" BorderThickness="1" Margin="-1" SnapsToDevicePixels="True" />
|
||||
<Grid x:Name="CanvasHost" LayoutUpdated="CanvasHost_LayoutUpdated">
|
||||
<Grid.LayoutTransform>
|
||||
<ScaleTransform x:Name="ZoomTransform" />
|
||||
</Grid.LayoutTransform>
|
||||
<local:AnnotationSurface x:Name="Surface"
|
||||
MouseLeftButtonDown="Surface_MouseLeftButtonDown"
|
||||
MouseMove="Surface_MouseMove"
|
||||
MouseLeave="Surface_MouseLeave"
|
||||
MouseLeftButtonUp="Surface_MouseLeftButtonUp"
|
||||
MouseRightButtonUp="Surface_MouseRightButtonUp"
|
||||
LostMouseCapture="Surface_LostMouseCapture" />
|
||||
<Canvas x:Name="TextLayer" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</ScrollViewer>
|
||||
</ScrollViewer>
|
||||
|
||||
<!-- Zwevende Nieuw en Screencast (drie-split); versleep aan de rand of het greepje -->
|
||||
<Canvas x:Name="FloatLayer" Grid.Row="1" Grid.Column="1" ClipToBounds="True" SizeChanged="FloatLayer_SizeChanged">
|
||||
<Border x:Name="CaptureFloat" Visibility="Collapsed" Background="{DynamicResource ToolbarBrush}"
|
||||
BorderBrush="{DynamicResource ControlStrongBorderBrush}" BorderThickness="1" CornerRadius="8" Padding="4,2,4,4"
|
||||
SizeChanged="FloatLayer_SizeChanged" ContextMenuOpening="Toolbar_ContextMenuOpening"
|
||||
MouseLeftButtonDown="CaptureFloat_MouseLeftButtonDown" MouseMove="CaptureFloat_MouseMove"
|
||||
MouseLeftButtonUp="CaptureFloat_MouseLeftButtonUp" LostMouseCapture="CaptureFloat_LostMouseCapture">
|
||||
<Border.Effect>
|
||||
<DropShadowEffect BlurRadius="12" ShadowDepth="2" Opacity="0.18" />
|
||||
</Border.Effect>
|
||||
<Border.ContextMenu>
|
||||
<ContextMenu x:Name="CaptureFloatMenu" />
|
||||
</Border.ContextMenu>
|
||||
<StackPanel x:Name="CaptureFloatPanel">
|
||||
<TextBlock x:Name="CaptureFloatGrip" Text="" FontFamily="Segoe Fluent Icons, Segoe MDL2 Assets" FontSize="12"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center"
|
||||
Foreground="{DynamicResource SubtleTextBrush}" Background="Transparent" Padding="0,2" Cursor="SizeAll"
|
||||
ToolTip="Slepen om te verplaatsen; rechtsklik om vast te zetten" />
|
||||
<Border x:Name="CaptureFloatHost" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Canvas>
|
||||
</Grid>
|
||||
</DockPanel>
|
||||
</Window>
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,64 @@
|
||||
<UserControl x:Class="Kniip.Editor.GradientAngleDial"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
VerticalAlignment="Center">
|
||||
<Grid>
|
||||
<ToggleButton x:Name="Toggle" Style="{StaticResource FlatButton}" Padding="6,4" MinWidth="0" MinHeight="30"
|
||||
BorderBrush="{DynamicResource ControlBorderBrush}" Background="{DynamicResource ControlBrush}"
|
||||
MouseWheel="Dial_MouseWheel">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Grid Width="20" Height="20">
|
||||
<Ellipse Stroke="{DynamicResource InkBrush}" StrokeThickness="1.2" Margin="1" />
|
||||
<Path x:Name="SmallArrow" Data="M10,15 V5 M7,8 L10,5 L13,8" Stroke="{DynamicResource InkBrush}" StrokeThickness="1.4"
|
||||
StrokeLineJoin="Round" StrokeStartLineCap="Round" StrokeEndLineCap="Round" RenderTransformOrigin="0.5,0.5">
|
||||
<Path.RenderTransform>
|
||||
<RotateTransform x:Name="SmallRotate" />
|
||||
</Path.RenderTransform>
|
||||
</Path>
|
||||
<Ellipse x:Name="SmallRadial" Width="5" Height="5" Fill="{DynamicResource InkBrush}" Visibility="Collapsed" />
|
||||
</Grid>
|
||||
<TextBlock Text="" Style="{StaticResource IconText}" FontSize="9" Margin="5,0,1,0" Foreground="{DynamicResource SubtleTextBrush}" />
|
||||
</StackPanel>
|
||||
</ToggleButton>
|
||||
<Popup x:Name="PopupPanel" PlacementTarget="{Binding ElementName=Toggle}" Placement="Bottom" StaysOpen="False"
|
||||
AllowsTransparency="True" PopupAnimation="Fade"
|
||||
IsOpen="{Binding IsChecked, ElementName=Toggle, Mode=TwoWay}">
|
||||
<Border Background="{DynamicResource CardBrush}" BorderBrush="{DynamicResource ControlStrongBorderBrush}" BorderThickness="1" CornerRadius="6" Padding="10" Margin="0,4,10,10">
|
||||
<Border.Effect>
|
||||
<DropShadowEffect BlurRadius="10" ShadowDepth="2" Opacity="0.18" />
|
||||
</Border.Effect>
|
||||
<StackPanel>
|
||||
<!-- Sleep de knop rond; klik ergens op de cirkel om de richting meteen te kiezen -->
|
||||
<Canvas x:Name="DialCanvas" Width="140" Height="140" Background="Transparent" Cursor="Hand"
|
||||
MouseLeftButtonDown="Dial_MouseDown" MouseMove="Dial_MouseMove" MouseLeftButtonUp="Dial_MouseUp"
|
||||
LostMouseCapture="Dial_LostMouseCapture" MouseWheel="Dial_MouseWheel">
|
||||
<Ellipse x:Name="Track" Canvas.Left="20" Canvas.Top="20" Width="100" Height="100"
|
||||
Stroke="{DynamicResource ControlStrongBorderBrush}" StrokeThickness="2" />
|
||||
<Canvas x:Name="Ticks" IsHitTestVisible="False" />
|
||||
<Line x:Name="Needle" X1="70" Y1="70" Stroke="{DynamicResource AccentBrush}" StrokeThickness="2"
|
||||
StrokeStartLineCap="Round" StrokeEndLineCap="Round" IsHitTestVisible="False" />
|
||||
<Ellipse Canvas.Left="66" Canvas.Top="66" Width="8" Height="8" Fill="{DynamicResource AccentBrush}" IsHitTestVisible="False" />
|
||||
<Ellipse x:Name="Knob" Width="16" Height="16" Fill="White" Stroke="{DynamicResource AccentBrush}" StrokeThickness="2"
|
||||
IsHitTestVisible="False" />
|
||||
</Canvas>
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,8,0,0">
|
||||
<TextBox x:Name="AngleBox" Width="48" VerticalContentAlignment="Center" HorizontalContentAlignment="Right" FontSize="12"
|
||||
Padding="2,3" KeyDown="AngleBox_KeyDown" LostKeyboardFocus="AngleBox_LostKeyboardFocus"
|
||||
ToolTip="Hoek in graden: 0 = naar boven, 90 = naar rechts, 180 = naar onder" />
|
||||
<TextBlock Text="°" FontSize="12" VerticalAlignment="Center" Margin="3,0,0,0" Foreground="{DynamicResource InkBrush}" />
|
||||
</StackPanel>
|
||||
<ToggleButton x:Name="RadialToggle" Style="{StaticResource ToolToggle}" Margin="0,8,0,0" HorizontalAlignment="Stretch"
|
||||
Click="Radial_Click" ToolTip="Verloop vanuit het midden naar buiten">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Path Data="M9,3 A6,6 0 1 1 8.99,3 Z M9,7.5 A1.5,1.5 0 1 1 8.99,7.5 Z" Stroke="{DynamicResource InkBrush}" StrokeThickness="1.4"
|
||||
Width="18" Height="18" VerticalAlignment="Center" />
|
||||
<TextBlock Text="Vanuit het midden" FontSize="12" VerticalAlignment="Center" Margin="6,0,0,0" />
|
||||
</StackPanel>
|
||||
</ToggleButton>
|
||||
<TextBlock Text="Shift = stappen van 15°, Alt = niet vastklikken" FontSize="11" Margin="0,6,0,0" HorizontalAlignment="Center"
|
||||
Foreground="{DynamicResource SubtleTextBrush}" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Popup>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,250 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Shapes;
|
||||
|
||||
namespace Kniip.Editor;
|
||||
|
||||
/// <summary>
|
||||
/// Richting van een verloop: een cirkel met een knop die je rondsleept, plus een hoekveld en "vanuit het midden".
|
||||
/// Hoeken in graden met de klok mee vanaf boven, zoals <see cref="CanvasBackground.Angle"/>.
|
||||
/// </summary>
|
||||
public partial class GradientAngleDial : UserControl
|
||||
{
|
||||
private const double Center = 70;
|
||||
private const double Radius = 50;
|
||||
|
||||
private double _angle = 180;
|
||||
private bool _isRadial;
|
||||
private bool _dragging;
|
||||
|
||||
/// <summary>Tijdens het slepen, voor een live voorbeeld; nog geen stap in Ongedaan maken.</summary>
|
||||
public event Action<double>? AngleChanging;
|
||||
|
||||
/// <summary>Definitieve hoek: bij loslaten, scrollen of typen. Kiest ook een lineair verloop.</summary>
|
||||
public event Action<double>? AngleCommitted;
|
||||
|
||||
/// <summary>"Vanuit het midden" aan- of uitgezet.</summary>
|
||||
public event Action<bool>? RadialChanged;
|
||||
|
||||
public GradientAngleDial()
|
||||
{
|
||||
InitializeComponent();
|
||||
BuildTicks();
|
||||
UpdateVisuals();
|
||||
}
|
||||
|
||||
public double Angle
|
||||
{
|
||||
get => _angle;
|
||||
set
|
||||
{
|
||||
_angle = Normalize(value);
|
||||
UpdateVisuals();
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsRadial
|
||||
{
|
||||
get => _isRadial;
|
||||
set
|
||||
{
|
||||
_isRadial = value;
|
||||
UpdateVisuals();
|
||||
}
|
||||
}
|
||||
|
||||
private static double Normalize(double angle)
|
||||
{
|
||||
angle %= 360;
|
||||
if (angle < 0)
|
||||
{
|
||||
angle += 360;
|
||||
}
|
||||
|
||||
return Math.Round(angle, 1) >= 360 ? 0 : Math.Round(angle, 1);
|
||||
}
|
||||
|
||||
private static Vector Direction(double angle)
|
||||
{
|
||||
double radians = angle * Math.PI / 180;
|
||||
return new Vector(Math.Sin(radians), -Math.Cos(radians));
|
||||
}
|
||||
|
||||
private void BuildTicks()
|
||||
{
|
||||
for (int i = 0; i < 8; i++)
|
||||
{
|
||||
Vector d = Direction(i * 45);
|
||||
double inner = Radius + 4, outer = Radius + (i % 2 == 0 ? 11 : 8);
|
||||
Ticks.Children.Add(new Line
|
||||
{
|
||||
X1 = Center + d.X * inner,
|
||||
Y1 = Center + d.Y * inner,
|
||||
X2 = Center + d.X * outer,
|
||||
Y2 = Center + d.Y * outer,
|
||||
StrokeThickness = 1.5,
|
||||
StrokeStartLineCap = PenLineCap.Round,
|
||||
StrokeEndLineCap = PenLineCap.Round
|
||||
});
|
||||
((Line)Ticks.Children[i]).SetResourceReference(Shape.StrokeProperty, "SubtleTextBrush");
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateVisuals()
|
||||
{
|
||||
Vector d = Direction(_angle);
|
||||
Point knob = new Point(Center, Center) + d * Radius;
|
||||
Canvas.SetLeft(Knob, knob.X - Knob.Width / 2);
|
||||
Canvas.SetTop(Knob, knob.Y - Knob.Height / 2);
|
||||
Needle.X2 = knob.X;
|
||||
Needle.Y2 = knob.Y;
|
||||
DialCanvas.Opacity = _isRadial ? 0.45 : 1;
|
||||
|
||||
SmallRotate.Angle = _angle;
|
||||
SmallArrow.Visibility = _isRadial ? Visibility.Collapsed : Visibility.Visible;
|
||||
SmallRadial.Visibility = _isRadial ? Visibility.Visible : Visibility.Collapsed;
|
||||
RadialToggle.IsChecked = _isRadial;
|
||||
if (!AngleBox.IsKeyboardFocusWithin)
|
||||
{
|
||||
AngleBox.Text = _angle.ToString("0.#", CultureInfo.CurrentCulture);
|
||||
}
|
||||
|
||||
Toggle.ToolTip = _isRadial
|
||||
? "Richting van het verloop: vanuit het midden"
|
||||
: $"Richting van het verloop: {_angle.ToString("0.#", CultureInfo.CurrentCulture)}° (scroll om te draaien)";
|
||||
}
|
||||
|
||||
private double AngleAt(Point p)
|
||||
{
|
||||
Vector v = p - new Point(Center, Center);
|
||||
if (v.Length < 1)
|
||||
{
|
||||
return _angle;
|
||||
}
|
||||
|
||||
double angle = Normalize(Math.Atan2(v.X, -v.Y) * 180 / Math.PI);
|
||||
ModifierKeys modifiers = Keyboard.Modifiers;
|
||||
if (modifiers.HasFlag(ModifierKeys.Alt))
|
||||
{
|
||||
return Normalize(Math.Round(angle));
|
||||
}
|
||||
|
||||
if (modifiers.HasFlag(ModifierKeys.Shift))
|
||||
{
|
||||
return Normalize(Math.Round(angle / 15) * 15);
|
||||
}
|
||||
|
||||
double nearest = Math.Round(angle / 45) * 45;
|
||||
return Normalize(Math.Abs(angle - nearest) <= 6 ? nearest : Math.Round(angle));
|
||||
}
|
||||
|
||||
private void Dial_MouseDown(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
double angle = AngleAt(e.GetPosition(DialCanvas));
|
||||
_dragging = DialCanvas.CaptureMouse();
|
||||
if (_dragging)
|
||||
{
|
||||
Preview(angle);
|
||||
}
|
||||
else
|
||||
{
|
||||
Commit(angle);
|
||||
}
|
||||
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
private void Dial_MouseMove(object sender, MouseEventArgs e)
|
||||
{
|
||||
if (_dragging)
|
||||
{
|
||||
Preview(AngleAt(e.GetPosition(DialCanvas)));
|
||||
}
|
||||
}
|
||||
|
||||
private void Dial_MouseUp(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (_dragging)
|
||||
{
|
||||
DialCanvas.ReleaseMouseCapture();
|
||||
}
|
||||
}
|
||||
|
||||
private void Dial_LostMouseCapture(object sender, MouseEventArgs e)
|
||||
{
|
||||
if (!_dragging)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_dragging = false;
|
||||
Commit(_angle);
|
||||
}
|
||||
|
||||
private void Dial_MouseWheel(object sender, MouseWheelEventArgs e)
|
||||
{
|
||||
double step = Keyboard.Modifiers.HasFlag(ModifierKeys.Alt) ? 1 : 15;
|
||||
double angle = e.Delta > 0 ? _angle + step : _angle - step;
|
||||
Commit(step == 15 ? Math.Round(angle / 15) * 15 : angle);
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
private void Preview(double angle)
|
||||
{
|
||||
_isRadial = false;
|
||||
Angle = angle;
|
||||
AngleChanging?.Invoke(_angle);
|
||||
}
|
||||
|
||||
private void Commit(double angle)
|
||||
{
|
||||
_isRadial = false;
|
||||
Angle = angle;
|
||||
AngleCommitted?.Invoke(_angle);
|
||||
}
|
||||
|
||||
private void AngleBox_KeyDown(object sender, KeyEventArgs e)
|
||||
{
|
||||
if (e.Key == Key.Enter)
|
||||
{
|
||||
ApplyTypedAngle();
|
||||
AngleBox.SelectAll();
|
||||
e.Handled = true;
|
||||
}
|
||||
else if (e.Key == Key.Escape)
|
||||
{
|
||||
AngleBox.Text = _angle.ToString("0.#", CultureInfo.CurrentCulture);
|
||||
Toggle.IsChecked = false;
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void AngleBox_LostKeyboardFocus(object sender, KeyboardFocusChangedEventArgs e) => ApplyTypedAngle();
|
||||
|
||||
private void ApplyTypedAngle()
|
||||
{
|
||||
string text = AngleBox.Text.Trim().TrimEnd('°');
|
||||
if (double.TryParse(text, NumberStyles.Float, CultureInfo.CurrentCulture, out double angle)
|
||||
|| double.TryParse(text, NumberStyles.Float, CultureInfo.InvariantCulture, out angle))
|
||||
{
|
||||
if (Normalize(angle) != _angle || _isRadial)
|
||||
{
|
||||
Commit(angle);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
AngleBox.Text = _angle.ToString("0.#", CultureInfo.CurrentCulture);
|
||||
}
|
||||
|
||||
private void Radial_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
_isRadial = RadialToggle.IsChecked == true;
|
||||
UpdateVisuals();
|
||||
RadialChanged?.Invoke(_isRadial);
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@ internal static class ImageEffects
|
||||
{
|
||||
public static BitmapSource Pixelate(EditorDocument doc, Int32Rect area, int blockSize)
|
||||
{
|
||||
byte[] src = doc.Pixels;
|
||||
byte[] src = doc.EffectPixels;
|
||||
int srcStride = doc.Width * 4;
|
||||
var dst = new byte[area.Width * area.Height * 4];
|
||||
int dstStride = area.Width * 4;
|
||||
@@ -20,21 +20,31 @@ internal static class ImageEffects
|
||||
for (int bx = 0; bx < area.Width; bx += blockSize)
|
||||
{
|
||||
int bw = Math.Min(blockSize, area.Width - bx);
|
||||
long b = 0, g = 0, r = 0;
|
||||
long b = 0, g = 0, r = 0, a = 0;
|
||||
for (int y = 0; y < bh; y++)
|
||||
{
|
||||
int row = (area.Y + by + y) * srcStride + (area.X + bx) * 4;
|
||||
for (int x = 0; x < bw; x++)
|
||||
{
|
||||
int i = row + x * 4;
|
||||
b += src[i];
|
||||
g += src[i + 1];
|
||||
r += src[i + 2];
|
||||
int alpha = src[i + 3];
|
||||
b += src[i] * alpha;
|
||||
g += src[i + 1] * alpha;
|
||||
r += src[i + 2] * alpha;
|
||||
a += alpha;
|
||||
}
|
||||
}
|
||||
|
||||
int count = bw * bh;
|
||||
byte ab = (byte)(b / count), ag = (byte)(g / count), ar = (byte)(r / count);
|
||||
byte ab = 0, ag = 0, ar = 0;
|
||||
if (a > 0)
|
||||
{
|
||||
ab = (byte)(b / a);
|
||||
ag = (byte)(g / a);
|
||||
ar = (byte)(r / a);
|
||||
}
|
||||
|
||||
byte aa = (byte)(a / count);
|
||||
for (int y = 0; y < bh; y++)
|
||||
{
|
||||
int row = (by + y) * dstStride + bx * 4;
|
||||
@@ -44,7 +54,7 @@ internal static class ImageEffects
|
||||
dst[i] = ab;
|
||||
dst[i + 1] = ag;
|
||||
dst[i + 2] = ar;
|
||||
dst[i + 3] = 255;
|
||||
dst[i + 3] = aa;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -69,7 +79,16 @@ internal static class ImageEffects
|
||||
int srcStride = doc.Width * 4;
|
||||
for (int y = 0; y < h; y++)
|
||||
{
|
||||
Buffer.BlockCopy(doc.Pixels, (top + y) * srcStride + left * 4, buffer, y * w * 4, w * 4);
|
||||
Buffer.BlockCopy(doc.EffectPixels, (top + y) * srcStride + left * 4, buffer, y * w * 4, w * 4);
|
||||
}
|
||||
|
||||
// Voorvermenigvuldigd vervagen, anders lopen de (zwarte) kleuren van transparante pixels mee.
|
||||
for (int i = 0; i < buffer.Length; i += 4)
|
||||
{
|
||||
int alpha = buffer[i + 3];
|
||||
buffer[i] = (byte)(buffer[i] * alpha / 255);
|
||||
buffer[i + 1] = (byte)(buffer[i + 1] * alpha / 255);
|
||||
buffer[i + 2] = (byte)(buffer[i + 2] * alpha / 255);
|
||||
}
|
||||
|
||||
var temp = new byte[buffer.Length];
|
||||
@@ -87,9 +106,15 @@ internal static class ImageEffects
|
||||
Buffer.BlockCopy(buffer, ((offsetY + y) * w + offsetX) * 4, dst, y * area.Width * 4, area.Width * 4);
|
||||
}
|
||||
|
||||
for (int i = 3; i < dst.Length; i += 4)
|
||||
for (int i = 0; i < dst.Length; i += 4)
|
||||
{
|
||||
dst[i] = 255;
|
||||
int alpha = dst[i + 3];
|
||||
if (alpha is > 0 and < 255)
|
||||
{
|
||||
dst[i] = (byte)Math.Min(255, dst[i] * 255 / alpha);
|
||||
dst[i + 1] = (byte)Math.Min(255, dst[i + 1] * 255 / alpha);
|
||||
dst[i + 2] = (byte)Math.Min(255, dst[i + 2] * 255 / alpha);
|
||||
}
|
||||
}
|
||||
|
||||
return PixelUtil.CreateBitmap(dst, area.Width, area.Height);
|
||||
@@ -101,7 +126,7 @@ internal static class ImageEffects
|
||||
for (int y = 0; y < h; y++)
|
||||
{
|
||||
int row = y * w * 4;
|
||||
for (int c = 0; c < 3; c++)
|
||||
for (int c = 0; c < 4; c++)
|
||||
{
|
||||
int sum = 0;
|
||||
for (int i = -r; i <= r; i++)
|
||||
@@ -127,7 +152,7 @@ internal static class ImageEffects
|
||||
for (int x = 0; x < w; x++)
|
||||
{
|
||||
int col = x * 4;
|
||||
for (int c = 0; c < 3; c++)
|
||||
for (int c = 0; c < 4; c++)
|
||||
{
|
||||
int sum = 0;
|
||||
for (int i = -r; i <= r; i++)
|
||||
|
||||
@@ -8,6 +8,7 @@ using System.Windows.Interop;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using Kniip.Capture;
|
||||
using Kniip.Infrastructure;
|
||||
using Kniip.Interop;
|
||||
|
||||
namespace Kniip.Editor;
|
||||
@@ -22,7 +23,7 @@ internal static class ImageExporter
|
||||
: new PngBitmapEncoder();
|
||||
|
||||
BitmapSource frame = encoder is JpegBitmapEncoder
|
||||
? new FormatConvertedBitmap(image, PixelFormats.Bgr24, null, 0)
|
||||
? new FormatConvertedBitmap(PixelUtil.OnWhite(image), PixelFormats.Bgr24, null, 0)
|
||||
: image;
|
||||
encoder.Frames.Add(BitmapFrame.Create(frame));
|
||||
|
||||
@@ -47,11 +48,12 @@ internal static class ClipboardWriter
|
||||
/// Zet de afbeelding als bitmap én als PNG op het klembord, met een markering zodat
|
||||
/// de eigen klembordbewaking dit niet als nieuw knipsel ziet.
|
||||
/// Met een pad staat ook het bestand zelf op het klembord.
|
||||
/// De bitmap krijgt wit onder transparante delen (veel programma's lezen daar de alpha niet van); de PNG houdt ze transparant.
|
||||
/// </summary>
|
||||
public static bool SetImage(BitmapSource image, string? filePath = null)
|
||||
{
|
||||
var data = new DataObject();
|
||||
data.SetImage(image);
|
||||
data.SetImage(PixelUtil.OnWhite(image));
|
||||
data.SetData("PNG", ImageExporter.EncodePng(image), false);
|
||||
data.SetData(ClipboardImageReader.ExportMarkerFormat, "1");
|
||||
if (!string.IsNullOrEmpty(filePath))
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
<Window x:Class="Kniip.Editor.OcrResultWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
Title="kniip tekst herkennen" Width="560" Height="420" MinWidth="360" MinHeight="240"
|
||||
WindowStartupLocation="CenterScreen" Background="{DynamicResource WindowBrush}" Foreground="{DynamicResource InkBrush}"
|
||||
FontFamily="Segoe UI" FontSize="13" Icon="pack://application:,,,/AppIcon.ico">
|
||||
<DockPanel Margin="14">
|
||||
<DockPanel DockPanel.Dock="Bottom" Margin="0,12,0,0" LastChildFill="False">
|
||||
<TextBlock x:Name="StatusText" DockPanel.Dock="Left" VerticalAlignment="Center" FontSize="12"
|
||||
Foreground="{DynamicResource SubtleTextBrush}" TextTrimming="CharacterEllipsis" />
|
||||
<Button DockPanel.Dock="Right" Style="{StaticResource SecondaryButton}" Content="Sluiten" IsCancel="True" Click="Close_Click" />
|
||||
<Button x:Name="CopyButton" DockPanel.Dock="Right" Style="{StaticResource AccentButton}" ToolTip="Alles kopiëren, of alleen de geselecteerde tekst"
|
||||
Click="Copy_Click">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Text="" Style="{StaticResource IconText}" FontSize="14" Foreground="White" />
|
||||
<TextBlock x:Name="CopyLabel" Text="Kopiëren" Margin="6,0,0,0" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</DockPanel>
|
||||
<Grid>
|
||||
<TextBox x:Name="ResultBox" AcceptsReturn="True" AcceptsTab="True" TextWrapping="Wrap" Padding="6"
|
||||
VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled"
|
||||
FontFamily="Segoe UI" FontSize="13" SelectionChanged="ResultBox_SelectionChanged" />
|
||||
<StackPanel x:Name="EmptyPanel" HorizontalAlignment="Center" VerticalAlignment="Center" Visibility="Collapsed" IsHitTestVisible="False">
|
||||
<TextBlock Text="" Style="{StaticResource IconText}" FontSize="28" Foreground="{DynamicResource SubtleTextBrush}" />
|
||||
<TextBlock x:Name="EmptyText" Text="Geen tekst gevonden in deze afbeelding." Margin="0,8,0,0" TextWrapping="Wrap"
|
||||
TextAlignment="Center" MaxWidth="380" Foreground="{DynamicResource SubtleTextBrush}" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</DockPanel>
|
||||
</Window>
|
||||
@@ -0,0 +1,110 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Media.Imaging;
|
||||
|
||||
namespace Kniip.Editor;
|
||||
|
||||
/// <summary>Toont de herkende tekst van een afbeelding, om aan te passen en te kopiëren.</summary>
|
||||
public partial class OcrResultWindow : Window
|
||||
{
|
||||
private OcrResultWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
Infrastructure.ThemeManager.Attach(this);
|
||||
}
|
||||
|
||||
/// <summary>Klaar zodra de tekst herkend is of het herkennen is mislukt.</summary>
|
||||
public Task Completion { get; private set; } = Task.CompletedTask;
|
||||
|
||||
/// <summary>Opent het venster meteen en vult het zodra de tekst herkend is.</summary>
|
||||
public static OcrResultWindow Open(BitmapSource image, Window? owner = null)
|
||||
{
|
||||
var window = new OcrResultWindow();
|
||||
if (owner is { IsVisible: true })
|
||||
{
|
||||
window.Owner = owner;
|
||||
window.WindowStartupLocation = WindowStartupLocation.CenterOwner;
|
||||
}
|
||||
|
||||
window.SetBusy(true);
|
||||
window.Show();
|
||||
window.Activate();
|
||||
window.Completion = window.RecognizeAsync(image);
|
||||
return window;
|
||||
}
|
||||
|
||||
private async Task RecognizeAsync(BitmapSource image)
|
||||
{
|
||||
try
|
||||
{
|
||||
ShowText(await OcrService.RecognizeAsync(image));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ShowError(ex is InvalidOperationException ? ex.Message : $"Tekst herkennen is mislukt.\n\n{ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
SetBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
private void SetBusy(bool busy)
|
||||
{
|
||||
ResultBox.IsReadOnly = busy;
|
||||
CopyButton.IsEnabled = !busy && ResultBox.Text.Length > 0;
|
||||
if (busy)
|
||||
{
|
||||
StatusText.Text = "Tekst herkennen...";
|
||||
}
|
||||
}
|
||||
|
||||
private void ShowText(string text)
|
||||
{
|
||||
ResultBox.Text = text;
|
||||
bool empty = text.Trim().Length == 0;
|
||||
EmptyPanel.Visibility = empty ? Visibility.Visible : Visibility.Collapsed;
|
||||
int lines = text.Split('\n').Count(line => line.Trim().Length > 0);
|
||||
StatusText.Text = empty ? string.Empty
|
||||
: $"{lines} {(lines == 1 ? "regel" : "regels")} herkend. Je kunt de tekst hier aanpassen.";
|
||||
if (!empty)
|
||||
{
|
||||
ResultBox.Focus();
|
||||
ResultBox.SelectAll();
|
||||
}
|
||||
}
|
||||
|
||||
private void ShowError(string message)
|
||||
{
|
||||
ResultBox.Text = string.Empty;
|
||||
ResultBox.IsEnabled = false;
|
||||
EmptyText.Text = message;
|
||||
EmptyPanel.Visibility = Visibility.Visible;
|
||||
StatusText.Text = string.Empty;
|
||||
}
|
||||
|
||||
private void ResultBox_SelectionChanged(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (!IsLoaded)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
CopyLabel.Text = ResultBox.SelectionLength > 0 && ResultBox.SelectionLength < ResultBox.Text.Length
|
||||
? "Selectie kopiëren"
|
||||
: "Kopiëren";
|
||||
CopyButton.IsEnabled = !ResultBox.IsReadOnly && ResultBox.Text.Length > 0;
|
||||
}
|
||||
|
||||
private void Copy_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
string text = ResultBox.SelectionLength > 0 ? ResultBox.SelectedText : ResultBox.Text;
|
||||
StatusText.Text = ClipboardWriter.SetText(text)
|
||||
? "Gekopieerd naar het klembord."
|
||||
: "Kopiëren mislukt: het klembord is bezet. Probeer het opnieuw.";
|
||||
}
|
||||
|
||||
private void Close_Click(object sender, RoutedEventArgs e) => Close();
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using Windows.Globalization;
|
||||
using Windows.Media.Ocr;
|
||||
using BitmapAlphaMode = Windows.Graphics.Imaging.BitmapAlphaMode;
|
||||
using BitmapPixelFormat = Windows.Graphics.Imaging.BitmapPixelFormat;
|
||||
using SoftwareBitmap = Windows.Graphics.Imaging.SoftwareBitmap;
|
||||
using WinBitmapDecoder = Windows.Graphics.Imaging.BitmapDecoder;
|
||||
|
||||
namespace Kniip.Editor;
|
||||
|
||||
/// <summary>Tekst herkennen met de OCR die in Windows zit; de taal volgt de talen van het gebruikersprofiel.</summary>
|
||||
internal static class OcrService
|
||||
{
|
||||
/// <summary>Kleinere afbeeldingen worden vergroot: kleine schermtekst mist de OCR anders vaak.</summary>
|
||||
private const int UpscaleBelowPixels = 2_000_000;
|
||||
|
||||
/// <summary>Herkende regels, gescheiden door regeleinden; leeg als er geen tekst is gevonden.</summary>
|
||||
/// <exception cref="InvalidOperationException">Er is geen taal voor tekstherkenning geïnstalleerd.</exception>
|
||||
public static async Task<string> RecognizeAsync(BitmapSource image)
|
||||
{
|
||||
OcrEngine engine = CreateEngine();
|
||||
using SoftwareBitmap bitmap = await ToSoftwareBitmapAsync(Prepare(image));
|
||||
OcrResult result = await engine.RecognizeAsync(bitmap);
|
||||
return Layout(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Regels opnieuw opbouwen uit de plek van elk woord. Windows geeft de tekst per blok terug; bij afwisselende
|
||||
/// kleuren of opmaak valt één regel dan in stukken die ver uit elkaar in het resultaat staan.
|
||||
/// </summary>
|
||||
private static string Layout(OcrResult result)
|
||||
{
|
||||
var words = result.Lines
|
||||
.SelectMany(line => line.Words)
|
||||
.Select(word => (word.Text, Box: new Rect(word.BoundingRect.X, word.BoundingRect.Y, word.BoundingRect.Width, word.BoundingRect.Height)))
|
||||
.Where(word => word.Text.Length > 0 && word.Box.Height > 0)
|
||||
.OrderBy(word => word.Box.Top + word.Box.Height / 2)
|
||||
.ToList();
|
||||
|
||||
var rows = new List<Row>();
|
||||
foreach ((string text, Rect box) in words)
|
||||
{
|
||||
double center = box.Top + box.Height / 2;
|
||||
Row? row = rows.LastOrDefault(r => center >= r.Top && center <= r.Bottom
|
||||
&& Math.Abs(r.Center - center) <= Math.Min(r.Height, box.Height) * 0.6);
|
||||
if (row == null)
|
||||
{
|
||||
row = new Row();
|
||||
rows.Add(row);
|
||||
}
|
||||
|
||||
row.Add(text, box);
|
||||
}
|
||||
|
||||
rows.Sort((a, b) => a.Center.CompareTo(b.Center));
|
||||
double lineHeight = rows.Count == 0 ? 0 : rows.Select(r => r.Height).OrderBy(h => h).ElementAt(rows.Count / 2);
|
||||
var output = new StringBuilder();
|
||||
for (int i = 0; i < rows.Count; i++)
|
||||
{
|
||||
if (i > 0)
|
||||
{
|
||||
output.AppendLine();
|
||||
// Een duidelijk grotere witruimte dan een gewone regelafstand: een lege regel tussen alinea's.
|
||||
if (rows[i].Top - rows[i - 1].Bottom > lineHeight * 0.9)
|
||||
{
|
||||
output.AppendLine();
|
||||
}
|
||||
}
|
||||
|
||||
output.Append(rows[i].Text);
|
||||
}
|
||||
|
||||
return output.ToString();
|
||||
}
|
||||
|
||||
private sealed class Row
|
||||
{
|
||||
private readonly List<(string Text, Rect Box)> _words = new();
|
||||
|
||||
public double Top { get; private set; } = double.MaxValue;
|
||||
|
||||
public double Bottom { get; private set; } = double.MinValue;
|
||||
|
||||
public double Height => Bottom - Top;
|
||||
|
||||
public double Center => (Top + Bottom) / 2;
|
||||
|
||||
public void Add(string text, Rect box)
|
||||
{
|
||||
_words.Add((text, box));
|
||||
Top = Math.Min(Top, box.Top);
|
||||
Bottom = Math.Max(Bottom, box.Bottom);
|
||||
}
|
||||
|
||||
public string Text => string.Join(" ", _words.OrderBy(w => w.Box.Left).Select(w => w.Text));
|
||||
}
|
||||
|
||||
private static OcrEngine CreateEngine()
|
||||
{
|
||||
if (OcrEngine.TryCreateFromUserProfileLanguages() is { } engine)
|
||||
{
|
||||
return engine;
|
||||
}
|
||||
|
||||
var english = new Language("en-US");
|
||||
if (OcrEngine.IsLanguageSupported(english) && OcrEngine.TryCreateFromLanguage(english) is { } fallback)
|
||||
{
|
||||
return fallback;
|
||||
}
|
||||
|
||||
throw new InvalidOperationException(
|
||||
"Windows heeft geen taal voor tekstherkenning. Voeg er een toe via Instellingen > Tijd en taal > Taal en regio: " +
|
||||
"kies een taal en zet bij de taalopties Optische tekenherkenning aan.");
|
||||
}
|
||||
|
||||
/// <summary>Op wit leggen (transparant leest de OCR als zwart) en zo nodig vergroten of verkleinen.</summary>
|
||||
private static BitmapSource Prepare(BitmapSource image)
|
||||
{
|
||||
int width = image.PixelWidth, height = image.PixelHeight;
|
||||
long pixels = (long)width * height;
|
||||
double scale = pixels < UpscaleBelowPixels / 4 ? 3 : pixels < UpscaleBelowPixels ? 2 : 1;
|
||||
double max = OcrEngine.MaxImageDimension;
|
||||
scale = Math.Min(scale, Math.Min(max / width, max / height));
|
||||
int w = Math.Max(1, (int)Math.Round(width * scale));
|
||||
int h = Math.Max(1, (int)Math.Round(height * scale));
|
||||
|
||||
var visual = new DrawingVisual();
|
||||
using (DrawingContext dc = visual.RenderOpen())
|
||||
{
|
||||
var area = new Rect(0, 0, w, h);
|
||||
dc.DrawRectangle(Brushes.White, null, area);
|
||||
dc.DrawImage(image, area);
|
||||
}
|
||||
|
||||
RenderOptions.SetBitmapScalingMode(visual, BitmapScalingMode.HighQuality);
|
||||
var target = new RenderTargetBitmap(w, h, 96, 96, PixelFormats.Pbgra32);
|
||||
target.Render(visual);
|
||||
target.Freeze();
|
||||
return target;
|
||||
}
|
||||
|
||||
private static async Task<SoftwareBitmap> ToSoftwareBitmapAsync(BitmapSource image)
|
||||
{
|
||||
var encoder = new PngBitmapEncoder();
|
||||
encoder.Frames.Add(BitmapFrame.Create(image));
|
||||
using var png = new MemoryStream();
|
||||
encoder.Save(png);
|
||||
png.Position = 0;
|
||||
|
||||
using var stream = png.AsRandomAccessStream();
|
||||
WinBitmapDecoder decoder = await WinBitmapDecoder.CreateAsync(stream);
|
||||
return await decoder.GetSoftwareBitmapAsync(BitmapPixelFormat.Bgra8, BitmapAlphaMode.Premultiplied);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace Kniip.Editor;
|
||||
|
||||
/// <summary>
|
||||
/// Liniaal langs het canvas met een schaalverdeling in pixels van de afbeelding.
|
||||
/// Sleep vanuit de liniaal om een hulplijn te maken; de editor regelt het slepen zelf.
|
||||
/// </summary>
|
||||
public sealed class RulerControl : FrameworkElement
|
||||
{
|
||||
private static readonly double[] Steps = { 1, 2, 5, 10, 20, 25, 50, 100, 200, 250, 500, 1000, 2000, 2500, 5000, 10000 };
|
||||
private static readonly Typeface LabelFace = new("Segoe UI");
|
||||
|
||||
public static readonly DependencyProperty BackgroundProperty = DependencyProperty.Register(
|
||||
nameof(Background), typeof(Brush), typeof(RulerControl), new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.AffectsRender));
|
||||
|
||||
public static readonly DependencyProperty ForegroundProperty = DependencyProperty.Register(
|
||||
nameof(Foreground), typeof(Brush), typeof(RulerControl), new FrameworkPropertyMetadata(Brushes.Gray, FrameworkPropertyMetadataOptions.AffectsRender));
|
||||
|
||||
public static readonly DependencyProperty BorderBrushProperty = DependencyProperty.Register(
|
||||
nameof(BorderBrush), typeof(Brush), typeof(RulerControl), new FrameworkPropertyMetadata(Brushes.LightGray, FrameworkPropertyMetadataOptions.AffectsRender));
|
||||
|
||||
public static readonly DependencyProperty ImageBackgroundProperty = DependencyProperty.Register(
|
||||
nameof(ImageBackground), typeof(Brush), typeof(RulerControl), new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.AffectsRender));
|
||||
|
||||
private double _origin;
|
||||
private double _scale = 1;
|
||||
private double _length;
|
||||
private double? _marker;
|
||||
private IReadOnlyList<double> _guides = Array.Empty<double>();
|
||||
|
||||
public Orientation Orientation { get; set; } = Orientation.Horizontal;
|
||||
|
||||
public Brush? Background
|
||||
{
|
||||
get => (Brush?)GetValue(BackgroundProperty);
|
||||
set => SetValue(BackgroundProperty, value);
|
||||
}
|
||||
|
||||
public Brush Foreground
|
||||
{
|
||||
get => (Brush)GetValue(ForegroundProperty);
|
||||
set => SetValue(ForegroundProperty, value);
|
||||
}
|
||||
|
||||
public Brush BorderBrush
|
||||
{
|
||||
get => (Brush)GetValue(BorderBrushProperty);
|
||||
set => SetValue(BorderBrushProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>Achtergrond van het stuk liniaal naast de afbeelding.</summary>
|
||||
public Brush? ImageBackground
|
||||
{
|
||||
get => (Brush?)GetValue(ImageBackgroundProperty);
|
||||
set => SetValue(ImageBackgroundProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>Plaats van afbeeldingspixel 0 in de liniaal (DIP), de schaal (DIP per pixel) en de lengte van de afbeelding in pixels.</summary>
|
||||
public void SetView(double origin, double scale, double length)
|
||||
{
|
||||
if (Math.Abs(origin - _origin) < 0.01 && Math.Abs(scale - _scale) < 1e-6 && Math.Abs(length - _length) < 0.01)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_origin = origin;
|
||||
_scale = scale;
|
||||
_length = length;
|
||||
InvalidateVisual();
|
||||
}
|
||||
|
||||
/// <summary>Positie van de muis in afbeeldingspixels, of null als de muis niet boven het canvas is.</summary>
|
||||
public void SetMarker(double? value)
|
||||
{
|
||||
if (_marker == value)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_marker = value;
|
||||
InvalidateVisual();
|
||||
}
|
||||
|
||||
public void SetGuides(IReadOnlyList<double> guides)
|
||||
{
|
||||
_guides = guides;
|
||||
InvalidateVisual();
|
||||
}
|
||||
|
||||
/// <summary>Afbeeldingscoördinaat bij een positie (DIP) in de liniaal.</summary>
|
||||
public double ToImage(double position) => (position - _origin) / _scale;
|
||||
|
||||
private bool Horizontal => Orientation == Orientation.Horizontal;
|
||||
|
||||
protected override void OnRender(DrawingContext dc)
|
||||
{
|
||||
double along = Horizontal ? ActualWidth : ActualHeight;
|
||||
double across = Horizontal ? ActualHeight : ActualWidth;
|
||||
if (along <= 0 || across <= 0 || _scale <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Ook transparant tekenen, zodat de hele liniaal muisklikken ontvangt.
|
||||
dc.DrawRectangle(Background ?? Brushes.Transparent, null, new Rect(0, 0, ActualWidth, ActualHeight));
|
||||
if (ImageBackground != null && _length > 0)
|
||||
{
|
||||
double start = Math.Max(0, _origin), end = Math.Min(along, _origin + _length * _scale);
|
||||
if (end > start)
|
||||
{
|
||||
dc.DrawRectangle(ImageBackground, null, Span(start, end - start, 0, across));
|
||||
}
|
||||
}
|
||||
|
||||
double dpi = VisualTreeHelper.GetDpi(this).PixelsPerDip;
|
||||
var tickPen = new Pen(Foreground, 1 / dpi);
|
||||
tickPen.Freeze();
|
||||
|
||||
double major = Steps[^1];
|
||||
foreach (double step in Steps)
|
||||
{
|
||||
if (step * _scale >= 50)
|
||||
{
|
||||
major = step;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
double minor = major;
|
||||
foreach (int divisions in new[] { 10, 5, 2 })
|
||||
{
|
||||
if (major % divisions == 0 && major / divisions * _scale >= 5)
|
||||
{
|
||||
minor = major / divisions;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
double first = Math.Floor(ToImage(0) / minor) * minor;
|
||||
double last = ToImage(along);
|
||||
for (double value = first; value <= last; value += minor)
|
||||
{
|
||||
double pos = Snap(_origin + value * _scale, dpi);
|
||||
bool isMajor = Math.Abs(value % major) < 1e-6;
|
||||
bool isHalf = !isMajor && Math.Abs(value % (major / 2)) < 1e-6;
|
||||
double tick = isMajor ? across : isHalf ? across * 0.45 : across * 0.25;
|
||||
dc.DrawLine(tickPen, Point(pos, across - tick), Point(pos, across));
|
||||
|
||||
if (isMajor)
|
||||
{
|
||||
DrawLabel(dc, value.ToString(CultureInfo.CurrentCulture), pos, dpi);
|
||||
}
|
||||
}
|
||||
|
||||
var guidePen = new Pen(new SolidColorBrush(AnnotationSurface.GuideColor), 1 / dpi);
|
||||
guidePen.Freeze();
|
||||
foreach (double guide in _guides)
|
||||
{
|
||||
double pos = Snap(_origin + guide * _scale, dpi);
|
||||
dc.DrawLine(guidePen, Point(pos, 0), Point(pos, across));
|
||||
}
|
||||
|
||||
if (_marker is double marker)
|
||||
{
|
||||
var markerPen = new Pen((Brush)FindResource("AccentBrush"), 1 / dpi);
|
||||
markerPen.Freeze();
|
||||
double pos = Snap(_origin + marker * _scale, dpi);
|
||||
dc.DrawLine(markerPen, Point(pos, 0), Point(pos, across));
|
||||
}
|
||||
|
||||
var border = new Pen(BorderBrush, 1 / dpi);
|
||||
border.Freeze();
|
||||
double edge = across - 0.5 / dpi;
|
||||
dc.DrawLine(border, Point(0, edge), Point(along, edge));
|
||||
}
|
||||
|
||||
private void DrawLabel(DrawingContext dc, string text, double pos, double dpi)
|
||||
{
|
||||
var ft = new FormattedText(text, CultureInfo.CurrentUICulture, FlowDirection.LeftToRight, LabelFace, 9, Foreground, dpi);
|
||||
if (Horizontal)
|
||||
{
|
||||
dc.DrawText(ft, new Point(pos + 3, 1));
|
||||
return;
|
||||
}
|
||||
|
||||
dc.PushTransform(new RotateTransform(-90, 1, pos - 3));
|
||||
dc.DrawText(ft, new Point(1, pos - 3));
|
||||
dc.Pop();
|
||||
}
|
||||
|
||||
private static double Snap(double value, double dpi) => (Math.Round(value * dpi) + 0.5) / dpi;
|
||||
|
||||
private Point Point(double along, double across) => Horizontal ? new Point(along, across) : new Point(across, along);
|
||||
|
||||
private Rect Span(double along, double alongLength, double across, double acrossLength) =>
|
||||
Horizontal ? new Rect(along, across, alongLength, acrossLength) : new Rect(across, along, acrossLength, alongLength);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Windows.Data;
|
||||
using Kniip.Editor.Annotations;
|
||||
|
||||
namespace Kniip.Editor;
|
||||
|
||||
/// <summary>Geeft de vectorvorm van een vormsoort, voor het icoon in de vormkeuze en op de werkbalkknop.</summary>
|
||||
public sealed class ShapeIconConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) =>
|
||||
ShapeGeometries.For(value is ShapeKind kind ? kind : ShapeKind.Triangle);
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) =>
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
@@ -16,7 +16,8 @@ public enum ToolKind
|
||||
Emoji,
|
||||
Highlight,
|
||||
Blur,
|
||||
Crop
|
||||
Crop,
|
||||
Shape
|
||||
}
|
||||
|
||||
public enum LineDash { Solid, Dash, Dot }
|
||||
@@ -29,6 +30,10 @@ public enum CursorKind { Arrow, Hand, Click }
|
||||
|
||||
public enum SkinTone { Default, Light, MediumLight, Medium, MediumDark, Dark }
|
||||
|
||||
public enum TextShape { Frame, Balloon, Oval, Cloud }
|
||||
|
||||
public enum ShapeKind { Triangle, BlockArrow, Star, Cloud, Balloon, Heart, Moon, Drop, Egg, Lightning, Spiral }
|
||||
|
||||
[Flags]
|
||||
public enum StyleProps
|
||||
{
|
||||
@@ -46,7 +51,9 @@ public enum StyleProps
|
||||
CursorKind = 1024,
|
||||
Blur = 2048,
|
||||
Straight = 4096,
|
||||
Emoji = 8192
|
||||
Emoji = 8192,
|
||||
TextShape = 16384,
|
||||
ShapeKind = 32768
|
||||
}
|
||||
|
||||
public sealed class StyleSet
|
||||
@@ -65,6 +72,11 @@ public sealed class StyleSet
|
||||
public BlurMode BlurMode { get; set; } = BlurMode.Pixelate;
|
||||
public int BlurStrength { get; set; } = 6;
|
||||
public CursorKind CursorKind { get; set; } = CursorKind.Arrow;
|
||||
public TextShape TextShape { get; set; } = TextShape.Frame;
|
||||
public ShapeKind ShapeKind { get; set; } = ShapeKind.Triangle;
|
||||
|
||||
/// <summary>Aantal rondjes in de staart van een wolk; 0 = automatisch naar de afstand.</summary>
|
||||
public int CloudBubbles { get; set; }
|
||||
|
||||
/// <summary>Unicode-codepunten van de emoji zonder huidskleur, zoals "1f44d".</summary>
|
||||
public string Emoji { get; set; } = "1f44d";
|
||||
@@ -97,9 +109,10 @@ public sealed class StyleSet
|
||||
{
|
||||
ToolKind.Rectangle => StyleProps.Stroke | StyleProps.Fill | StyleProps.Thickness | StyleProps.Dash | StyleProps.Corner | StyleProps.Shadow,
|
||||
ToolKind.Ellipse => StyleProps.Stroke | StyleProps.Fill | StyleProps.Thickness | StyleProps.Dash | StyleProps.Shadow,
|
||||
ToolKind.Shape => StyleProps.Stroke | StyleProps.Fill | StyleProps.Thickness | StyleProps.Dash | StyleProps.Shadow | StyleProps.ShapeKind,
|
||||
ToolKind.Line or ToolKind.Arrow => StyleProps.Stroke | StyleProps.Thickness | StyleProps.Dash | StyleProps.Shadow,
|
||||
ToolKind.Text => StyleProps.Stroke | StyleProps.Fill | StyleProps.Foreground | StyleProps.Thickness | StyleProps.Dash
|
||||
| StyleProps.Corner | StyleProps.Shadow | StyleProps.Font,
|
||||
| StyleProps.Corner | StyleProps.Shadow | StyleProps.Font | StyleProps.TextShape,
|
||||
ToolKind.Step => StyleProps.Fill | StyleProps.Foreground | StyleProps.Numbering | StyleProps.Size | StyleProps.Shadow,
|
||||
ToolKind.Cursor => StyleProps.CursorKind | StyleProps.Size | StyleProps.Shadow,
|
||||
ToolKind.Emoji => StyleProps.Emoji | StyleProps.Size | StyleProps.Shadow,
|
||||
|
||||
@@ -29,6 +29,72 @@ internal static class PixelUtil
|
||||
return pixels;
|
||||
}
|
||||
|
||||
/// <summary>Zet een afbeelding om naar Bgra32 en behoudt de transparantie.</summary>
|
||||
public static byte[] ToBgra(BitmapSource source)
|
||||
{
|
||||
BitmapSource converted = source.Format == PixelFormats.Bgra32
|
||||
? source
|
||||
: new FormatConvertedBitmap(source, PixelFormats.Bgra32, null, 0);
|
||||
|
||||
int stride = converted.PixelWidth * 4;
|
||||
var pixels = new byte[stride * converted.PixelHeight];
|
||||
converted.CopyPixels(pixels, stride, 0);
|
||||
return pixels;
|
||||
}
|
||||
|
||||
public static bool HasTransparency(byte[] bgra)
|
||||
{
|
||||
for (int i = 3; i < bgra.Length; i += 4)
|
||||
{
|
||||
if (bgra[i] != 255)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Een afbeelding waarvan alle pixels alpha 0 hebben, is vrijwel altijd een DIB zonder echte alpha.
|
||||
/// </summary>
|
||||
public static bool IsFullyTransparent(BitmapSource source)
|
||||
{
|
||||
if (!source.Format.Equals(PixelFormats.Bgra32) && !source.Format.Equals(PixelFormats.Pbgra32))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
byte[] pixels = ToBgra(source);
|
||||
for (int i = 3; i < pixels.Length; i += 4)
|
||||
{
|
||||
if (pixels[i] != 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>Transparante delen wit maken, voor formaten en programma's zonder alpha (JPEG, bitmap op het klembord).</summary>
|
||||
public static BitmapSource OnWhite(BitmapSource image)
|
||||
{
|
||||
var visual = new DrawingVisual();
|
||||
using (DrawingContext dc = visual.RenderOpen())
|
||||
{
|
||||
var area = new System.Windows.Rect(0, 0, image.PixelWidth, image.PixelHeight);
|
||||
dc.DrawRectangle(Brushes.White, null, area);
|
||||
dc.DrawImage(image, area);
|
||||
}
|
||||
|
||||
var bitmap = new RenderTargetBitmap(image.PixelWidth, image.PixelHeight, 96, 96, PixelFormats.Pbgra32);
|
||||
bitmap.Render(visual);
|
||||
var opaque = new FormatConvertedBitmap(bitmap, PixelFormats.Bgr32, null, 0);
|
||||
opaque.Freeze();
|
||||
return opaque;
|
||||
}
|
||||
|
||||
public static BitmapSource CreateBitmap(byte[] pixels, int width, int height)
|
||||
{
|
||||
var bitmap = BitmapSource.Create(width, height, 96, 96, PixelFormats.Bgra32, null, pixels, width * 4);
|
||||
|
||||
@@ -17,6 +17,7 @@ internal sealed class TrayIcon : IDisposable
|
||||
private readonly ToolStripMenuItem _pauseItem;
|
||||
private readonly ToolStripMenuItem _snipItem;
|
||||
private readonly ToolStripMenuItem _windowItem;
|
||||
private readonly ToolStripMenuItem _ocrItem;
|
||||
private readonly ToolStripMenuItem _recordItem;
|
||||
private readonly ToolStripMenuItem _gifItem;
|
||||
private readonly ToolStripMenuItem _foldersItem;
|
||||
@@ -35,6 +36,7 @@ internal sealed class TrayIcon : IDisposable
|
||||
public event Action? OpenFromClipboardRequested;
|
||||
public event Action? SnipRequested;
|
||||
public event Action? WindowSnipRequested;
|
||||
public event Action? TextRecognitionRequested;
|
||||
public event Action? RecordRequested;
|
||||
public event Action? GifRequested;
|
||||
public event Action? OpenFileRequested;
|
||||
@@ -51,17 +53,22 @@ internal sealed class TrayIcon : IDisposable
|
||||
public TrayIcon()
|
||||
{
|
||||
var menu = new ContextMenuStrip();
|
||||
_clipboardItem = new ToolStripMenuItem("Bewerken vanaf klembord", null, (_, _) => OpenFromClipboardRequested?.Invoke())
|
||||
{
|
||||
Font = new Font(SystemFonts.MenuFont ?? Control.DefaultFont, FontStyle.Bold)
|
||||
};
|
||||
_clipboardItem = new ToolStripMenuItem("Bewerken vanaf klembord", null, (_, _) => OpenFromClipboardRequested?.Invoke());
|
||||
menu.Items.Add(_clipboardItem);
|
||||
menu.Items.Add("Afbeelding openen...", null, (_, _) => OpenFileRequested?.Invoke());
|
||||
menu.Items.Add(new ToolStripSeparator());
|
||||
_snipItem = new ToolStripMenuItem("Rechthoek knippen", null, (_, _) => SnipRequested?.Invoke());
|
||||
_snipItem = new ToolStripMenuItem("Rechthoek knippen", null, (_, _) => SnipRequested?.Invoke())
|
||||
{
|
||||
Font = new Font(SystemFonts.MenuFont ?? Control.DefaultFont, FontStyle.Bold)
|
||||
};
|
||||
menu.Items.Add(_snipItem);
|
||||
_windowItem = new ToolStripMenuItem("Venster knippen", null, (_, _) => WindowSnipRequested?.Invoke());
|
||||
menu.Items.Add(_windowItem);
|
||||
_ocrItem = new ToolStripMenuItem("OCR (tekst herkennen)...", null, (_, _) => TextRecognitionRequested?.Invoke())
|
||||
{
|
||||
ToolTipText = "Sleep een rechthoek; de tekst daarin opent in een apart venster om te kopiëren"
|
||||
};
|
||||
menu.Items.Add(_ocrItem);
|
||||
_delayItem = BuildDelayItem();
|
||||
menu.Items.Add(_delayItem);
|
||||
_recordItem = new ToolStripMenuItem(RecordText, null, (_, _) => RecordRequested?.Invoke());
|
||||
@@ -100,7 +107,7 @@ internal sealed class TrayIcon : IDisposable
|
||||
{
|
||||
if (e.Button == MouseButtons.Left)
|
||||
{
|
||||
OpenFromClipboardRequested?.Invoke();
|
||||
SnipRequested?.Invoke();
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -108,9 +115,10 @@ internal sealed class TrayIcon : IDisposable
|
||||
public bool IsPaused => _pauseItem.Checked;
|
||||
|
||||
/// <summary>Toont de sneltoetsen naast de menu-items; null = geen sneltoets.</summary>
|
||||
public void SetHotkeys(string? snip, string? window, string? record, string? gif, string? clipboard)
|
||||
public void SetHotkeys(string? snip, string? window, string? record, string? gif, string? clipboard, string? ocr)
|
||||
{
|
||||
_clipboardItem.ShortcutKeyDisplayString = clipboard;
|
||||
_ocrItem.ShortcutKeyDisplayString = ocr;
|
||||
_snipItem.ShortcutKeyDisplayString = snip;
|
||||
_windowItem.ShortcutKeyDisplayString = window;
|
||||
_recordItem.ShortcutKeyDisplayString = record;
|
||||
|
||||
@@ -18,7 +18,10 @@
|
||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||
<Product>kniip</Product>
|
||||
<Description>Annotatie-editor naast het Windows Knipprogramma</Description>
|
||||
<Version>1.1.7</Version>
|
||||
<Version>2.1.0</Version>
|
||||
<Authors>Rogier Meijer</Authors>
|
||||
<Copyright>Copyright © 2026 Rogier Meijer</Copyright>
|
||||
<PackageLicenseExpression>GPL-3.0-or-later</PackageLicenseExpression>
|
||||
<SatelliteResourceLanguages>nl;en</SatelliteResourceLanguages>
|
||||
<!-- WPF regelt DPI via app.manifest; de WinForms-analyzer is hier niet van toepassing. -->
|
||||
<NoWarn>$(NoWarn);WFAC010</NoWarn>
|
||||
|
||||
@@ -111,6 +111,9 @@
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Text="" Style="{StaticResource IconText}" FontSize="14" Margin="0,0,6,0" />
|
||||
<TextBlock x:Name="SaveText" Text="Opslaan als MP4..." VerticalAlignment="Center" />
|
||||
<TextBlock Text="" Style="{StaticResource IconText}" FontSize="9"
|
||||
Background="Transparent" Padding="4,2,0,2" Margin="6,0,0,0"
|
||||
ToolTip="Meer opslagopties" PreviewMouseLeftButtonDown="SaveMenu_MouseDown" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
|
||||
@@ -8,6 +8,7 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Controls.Primitives;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Threading;
|
||||
using Kniip.Editor;
|
||||
@@ -346,13 +347,13 @@ public partial class ScreencastWindow : Window
|
||||
Mp4Toggle.IsChecked = format == ScreencastFormat.Mp4;
|
||||
GifToggle.IsChecked = format == ScreencastFormat.Gif;
|
||||
GifOptions.Visibility = format == ScreencastFormat.Gif ? Visibility.Visible : Visibility.Collapsed;
|
||||
bool copy = _settings.SaveButton == SaveButtonChoice.SaveAndCopy;
|
||||
bool copy = CopiesOnSave;
|
||||
string ellipsis = string.IsNullOrEmpty(_settings.VideoSaveFolder) ? "..." : string.Empty;
|
||||
string action = copy ? "Opslaan en kopiëren als " : "Opslaan als ";
|
||||
SaveText.Text = action + (format == ScreencastFormat.Gif ? "GIF" : "MP4") + ellipsis;
|
||||
SaveButton.ToolTip = copy
|
||||
? "Slaat op en kopieert het bestand naar het klembord. Shift+klik = altijd vragen waar je opslaat."
|
||||
: "Shift+klik = altijd vragen waar je opslaat";
|
||||
? "Slaat op en kopieert het bestand naar het klembord. Shift+klik = altijd vragen waar je opslaat; het pijltje toont meer opties."
|
||||
: "Shift+klik = altijd vragen waar je opslaat; het pijltje toont meer opties";
|
||||
UploadButton.Visibility = _settings.Uploader is { IsConfigured: true } ? Visibility.Visible : Visibility.Collapsed;
|
||||
UpdateInfo();
|
||||
}
|
||||
@@ -395,7 +396,36 @@ public partial class ScreencastWindow : Window
|
||||
private static string DefaultFolder =>
|
||||
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyVideos), "kniip");
|
||||
|
||||
private async void Save_Click(object sender, RoutedEventArgs e)
|
||||
private bool CopiesOnSave => _settings.SaveButton == SaveButtonChoice.SaveAndCopy;
|
||||
|
||||
private async void Save_Click(object sender, RoutedEventArgs e) =>
|
||||
await SaveAsync(Keyboard.Modifiers.HasFlag(ModifierKeys.Shift), CopiesOnSave);
|
||||
|
||||
private void SaveMenu_MouseDown(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
e.Handled = true;
|
||||
if (IsBusy || !SaveButton.IsEnabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
bool copy = CopiesOnSave;
|
||||
string format = Format == ScreencastFormat.Gif ? "GIF" : "MP4";
|
||||
string ellipsis = string.IsNullOrEmpty(_settings.VideoSaveFolder) ? "..." : string.Empty;
|
||||
var menu = new ContextMenu { PlacementTarget = SaveButton, Placement = PlacementMode.Bottom };
|
||||
menu.Items.Add(SaveMenuItem((copy ? "Opslaan als " : "Opslaan en kopiëren als ") + format + ellipsis, false, !copy));
|
||||
menu.Items.Add(SaveMenuItem("Opslaan als...", true, copy));
|
||||
menu.IsOpen = true;
|
||||
}
|
||||
|
||||
private MenuItem SaveMenuItem(string header, bool ask, bool copy)
|
||||
{
|
||||
var item = new MenuItem { Header = header };
|
||||
item.Click += async (_, _) => await SaveAsync(ask, copy);
|
||||
return item;
|
||||
}
|
||||
|
||||
private async Task SaveAsync(bool ask, bool copy)
|
||||
{
|
||||
if (IsBusy)
|
||||
{
|
||||
@@ -403,7 +433,6 @@ public partial class ScreencastWindow : Window
|
||||
}
|
||||
|
||||
SetPlaying(false);
|
||||
bool ask = Keyboard.Modifiers.HasFlag(ModifierKeys.Shift);
|
||||
string? target = ask ? null : FixedTarget();
|
||||
if (target is null)
|
||||
{
|
||||
@@ -421,7 +450,6 @@ public partial class ScreencastWindow : Window
|
||||
_kept = true;
|
||||
RememberChoices();
|
||||
AddToHistory(target, null, target);
|
||||
bool copy = _settings.SaveButton == SaveButtonChoice.SaveAndCopy;
|
||||
bool copied = !copy || ClipboardWriter.SetFile(target);
|
||||
App.Instance.ShowBalloon(copy && copied ? "Screencast opgeslagen en gekopieerd" : "Screencast opgeslagen", target);
|
||||
if (!copied)
|
||||
|
||||
@@ -39,7 +39,10 @@ public enum EditorLayout
|
||||
Single,
|
||||
|
||||
/// <summary>Split-weergave: gereedschappen boven, opslaan/kopiëren en zoom onder.</summary>
|
||||
Split
|
||||
Split,
|
||||
|
||||
/// <summary>Als Split, met Nieuw en Screencast in een zijbalk naast het canvas.</summary>
|
||||
ThreeSplit
|
||||
}
|
||||
|
||||
public enum ColorMode
|
||||
@@ -85,6 +88,9 @@ public sealed class AppSettings
|
||||
|
||||
/// <summary>Sneltoets om de afbeelding op het klembord in de editor te openen; standaard geen.</summary>
|
||||
public string ClipboardHotkey { get; set; } = "";
|
||||
|
||||
/// <summary>Sneltoets om een rechthoek te kiezen en de tekst erin te herkennen; standaard geen.</summary>
|
||||
public string OcrHotkey { get; set; } = "";
|
||||
public bool RecordSystemAudio { get; set; } = true;
|
||||
public bool RecordMicrophone { get; set; }
|
||||
public bool RecordShowCursor { get; set; } = true;
|
||||
@@ -122,8 +128,26 @@ public sealed class AppSettings
|
||||
/// <summary>Volgorde van de werkbalkitems in de gesplitste indeling (boven, dan onder); leeg betekent de standaardvolgorde.</summary>
|
||||
public List<string> SplitToolbarOrder { get; set; } = new();
|
||||
|
||||
/// <summary>Kant van het canvas waar de balk met Nieuw en Screencast van de drie-split-indeling staat.</summary>
|
||||
public Dock CaptureBarPosition { get; set; } = Dock.Left;
|
||||
|
||||
/// <summary>Nieuw en Screencast zwevend boven het canvas tonen in plaats van in een balk aan CaptureBarPosition.</summary>
|
||||
public bool CaptureBarFloating { get; set; }
|
||||
|
||||
/// <summary>Positie van de zwevende knoppen als fractie (0-1) van de vrije ruimte boven het canvas, horizontaal.</summary>
|
||||
public double CaptureBarFloatX { get; set; }
|
||||
|
||||
/// <summary>Positie van de zwevende knoppen als fractie (0-1) van de vrije ruimte boven het canvas, verticaal.</summary>
|
||||
public double CaptureBarFloatY { get; set; } = 0.5;
|
||||
|
||||
/// <summary>De zwevende knoppen staan vast en kunnen niet versleept worden.</summary>
|
||||
public bool CaptureBarFloatLocked { get; set; }
|
||||
|
||||
public List<string> ToolbarHidden { get; set; } = new();
|
||||
|
||||
/// <summary>Linialen en hulplijnen in de editor tonen; objecten klikken dan ook vast aan hulplijnen en de randen en het midden van de afbeelding.</summary>
|
||||
public bool ShowRulers { get; set; } = true;
|
||||
|
||||
/// <summary>Zipline-server waar Uploaden naartoe gaat; leeg betekent geen uploadknop.</summary>
|
||||
public UploaderConfig? Uploader { get; set; }
|
||||
|
||||
|
||||
@@ -131,13 +131,20 @@
|
||||
<Button Tag="Gif" Style="{StaticResource FlatButton}" Content="Geen" Click="ClearHotkey_Click" Margin="6,0,0,0"
|
||||
ToolTip="Geen sneltoets gebruiken" />
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,0,0,6">
|
||||
<StackPanel Orientation="Horizontal" Margin="0,0,0,4">
|
||||
<TextBlock Text="Vanaf klembord" VerticalAlignment="Center" Width="150" />
|
||||
<Button x:Name="ClipboardHotkeyButton" Tag="Clipboard" Style="{StaticResource SecondaryButton}" Width="170" Margin="0"
|
||||
Click="HotkeyButton_Click" ToolTip="Klik en druk daarna de gewenste toetscombinatie in; opent de afbeelding op het klembord" />
|
||||
<Button Tag="Clipboard" Style="{StaticResource FlatButton}" Content="Geen" Click="ClearHotkey_Click" Margin="6,0,0,0"
|
||||
ToolTip="Geen sneltoets gebruiken" />
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,0,0,6">
|
||||
<TextBlock Text="OCR (tekst herkennen)" VerticalAlignment="Center" Width="150" />
|
||||
<Button x:Name="OcrHotkeyButton" Tag="Ocr" Style="{StaticResource SecondaryButton}" Width="170" Margin="0"
|
||||
Click="HotkeyButton_Click" ToolTip="Klik en druk daarna de gewenste toetscombinatie in; sleep daarna een rechthoek om de tekst erin te lezen" />
|
||||
<Button Tag="Ocr" Style="{StaticResource FlatButton}" Content="Geen" Click="ClearHotkey_Click" Margin="6,0,0,0"
|
||||
ToolTip="Geen sneltoets gebruiken" />
|
||||
</StackPanel>
|
||||
<TextBlock x:Name="HotkeyHint" Style="{StaticResource Hint}" Margin="0,0,0,4" />
|
||||
<TextBlock Text="Gebied kiezen" Style="{StaticResource CardTitle}" Margin="0,12,0,8" />
|
||||
<CheckBox x:Name="MagnifierBox" Content="Vergrootglas tonen bij het kiezen van een gebied (knipsel en screencast)" />
|
||||
@@ -239,6 +246,14 @@
|
||||
<CheckBox x:Name="StraightHighlightBox" Content="Markeerstift trekt standaard een rechte horizontale streep (Shift wisselt)" />
|
||||
<TextBlock Text="Werkbalk" Style="{StaticResource CardTitle}" Margin="0,16,0,6" />
|
||||
<RadioButton x:Name="LayoutSplitRadio" GroupName="EditorLayout" Content="Split-weergave: gereedschappen boven, opslaan onder" />
|
||||
<RadioButton x:Name="LayoutThreeSplitRadio" GroupName="EditorLayout" Content="Drie-split: gereedschappen boven, opslaan onder, Nieuw en Screencast apart" />
|
||||
<StackPanel Orientation="Horizontal" Margin="22,2,0,4" IsEnabled="{Binding IsChecked, ElementName=LayoutThreeSplitRadio}">
|
||||
<RadioButton x:Name="CaptureBarLeftRadio" GroupName="CaptureBarPosition" Content="Links" Margin="0,0,16,0" />
|
||||
<RadioButton x:Name="CaptureBarRightRadio" GroupName="CaptureBarPosition" Content="Rechts" Margin="0,0,16,0" />
|
||||
<RadioButton x:Name="CaptureBarTopRadio" GroupName="CaptureBarPosition" Content="Boven" Margin="0,0,16,0" />
|
||||
<RadioButton x:Name="CaptureBarBottomRadio" GroupName="CaptureBarPosition" Content="Onder" Margin="0,0,16,0" />
|
||||
<RadioButton x:Name="CaptureBarFloatRadio" GroupName="CaptureBarPosition" Content="Zwevend" />
|
||||
</StackPanel>
|
||||
<RadioButton x:Name="LayoutSingleRadio" GroupName="EditorLayout" Content="Eén werkbalk met gereedschappen en acties" />
|
||||
<TextBlock Style="{StaticResource Hint}"
|
||||
Text="Ook te wisselen via rechtsklik op de werkbalk in de editor. Daar kun je knoppen ook verslepen of verbergen." />
|
||||
|
||||
@@ -27,6 +27,7 @@ public partial class SettingsWindow : Window
|
||||
["Record"] = "screencast (MP4)",
|
||||
["Gif"] = "screencast (GIF)",
|
||||
["Clipboard"] = "bewerken vanaf klembord",
|
||||
["Ocr"] = "OCR",
|
||||
};
|
||||
|
||||
private readonly AppSettings _settings;
|
||||
@@ -66,6 +67,7 @@ public partial class SettingsWindow : Window
|
||||
["Record"] = RecordHotkeyButton,
|
||||
["Gif"] = GifHotkeyButton,
|
||||
["Clipboard"] = ClipboardHotkeyButton,
|
||||
["Ocr"] = OcrHotkeyButton,
|
||||
};
|
||||
|
||||
HotkeyHint.Text = App.Instance.HotkeyHook?.IsActive == true
|
||||
@@ -102,7 +104,14 @@ public partial class SettingsWindow : Window
|
||||
ColorModeDarkRadio.IsChecked = settings.ColorMode == ColorMode.Dark;
|
||||
ColorModeSystemRadio.IsChecked = settings.ColorMode == ColorMode.System;
|
||||
LayoutSplitRadio.IsChecked = settings.EditorLayout == EditorLayout.Split;
|
||||
LayoutSingleRadio.IsChecked = settings.EditorLayout != EditorLayout.Split;
|
||||
LayoutThreeSplitRadio.IsChecked = settings.EditorLayout == EditorLayout.ThreeSplit;
|
||||
LayoutSingleRadio.IsChecked = settings.EditorLayout == EditorLayout.Single;
|
||||
bool docked = !settings.CaptureBarFloating;
|
||||
CaptureBarLeftRadio.IsChecked = docked && settings.CaptureBarPosition == Dock.Left;
|
||||
CaptureBarRightRadio.IsChecked = docked && settings.CaptureBarPosition == Dock.Right;
|
||||
CaptureBarTopRadio.IsChecked = docked && settings.CaptureBarPosition == Dock.Top;
|
||||
CaptureBarBottomRadio.IsChecked = docked && settings.CaptureBarPosition == Dock.Bottom;
|
||||
CaptureBarFloatRadio.IsChecked = settings.CaptureBarFloating;
|
||||
AutoUpdateCheckBox.IsChecked = settings.CheckForUpdates;
|
||||
MagnifierBox.IsChecked = settings.ShowMagnifier;
|
||||
|
||||
@@ -120,6 +129,7 @@ public partial class SettingsWindow : Window
|
||||
_hotkeys["Record"] = settings.RecordHotkey;
|
||||
_hotkeys["Gif"] = settings.GifHotkey;
|
||||
_hotkeys["Clipboard"] = settings.ClipboardHotkey;
|
||||
_hotkeys["Ocr"] = settings.OcrHotkey;
|
||||
foreach (string id in _hotkeys.Keys)
|
||||
{
|
||||
ShowHotkey(id);
|
||||
@@ -451,7 +461,17 @@ public partial class SettingsWindow : Window
|
||||
_settings.ColorMode = ColorModeDarkRadio.IsChecked == true ? ColorMode.Dark
|
||||
: ColorModeSystemRadio.IsChecked == true ? ColorMode.System
|
||||
: ColorMode.Light;
|
||||
_settings.EditorLayout = LayoutSplitRadio.IsChecked == true ? EditorLayout.Split : EditorLayout.Single;
|
||||
_settings.EditorLayout = LayoutSplitRadio.IsChecked == true ? EditorLayout.Split
|
||||
: LayoutThreeSplitRadio.IsChecked == true ? EditorLayout.ThreeSplit
|
||||
: EditorLayout.Single;
|
||||
_settings.CaptureBarFloating = CaptureBarFloatRadio.IsChecked == true;
|
||||
if (!_settings.CaptureBarFloating)
|
||||
{
|
||||
_settings.CaptureBarPosition = CaptureBarRightRadio.IsChecked == true ? Dock.Right
|
||||
: CaptureBarTopRadio.IsChecked == true ? Dock.Top
|
||||
: CaptureBarBottomRadio.IsChecked == true ? Dock.Bottom
|
||||
: Dock.Left;
|
||||
}
|
||||
_settings.CheckForUpdates = AutoUpdateCheckBox.IsChecked == true;
|
||||
_settings.ShowMagnifier = MagnifierBox.IsChecked == true;
|
||||
StopListening();
|
||||
@@ -460,6 +480,7 @@ public partial class SettingsWindow : Window
|
||||
_settings.RecordHotkey = _hotkeys["Record"];
|
||||
_settings.GifHotkey = _hotkeys["Gif"];
|
||||
_settings.ClipboardHotkey = _hotkeys["Clipboard"];
|
||||
_settings.OcrHotkey = _hotkeys["Ocr"];
|
||||
_settings.RecordSystemAudio = SystemAudioBox.IsChecked == true;
|
||||
_settings.RecordMicrophone = MicrophoneBox.IsChecked == true;
|
||||
_settings.RecordShowCursor = ShowCursorBox.IsChecked == true;
|
||||
|
||||
Reference in new issue
Block a user