Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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,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,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,53 @@
|
||||
|
||||
Alle noemenswaardige wijzigingen in kniip staan in dit bestand. De nieuwste versie staat bovenaan.
|
||||
|
||||
## 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
|
||||
|
||||
@@ -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) |
|
||||
| 1.1.9 | [Kniip-Setup-1.1.9.exe](https://git.hibbit.net/rogier/kniip/releases/download/1.1.9/Kniip-Setup-1.1.9.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 1.1.9
|
||||
.\build.ps1 -Version 1.2.0 # andere versie
|
||||
```
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
# .\build.ps1 -SkipTest -> zonder testexe
|
||||
|
||||
param(
|
||||
[string]$Version = "1.1.7",
|
||||
[string]$Version = "1.1.9",
|
||||
[string]$Runtime = "win-x64",
|
||||
[switch]$SkipTest
|
||||
)
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@
|
||||
#define AppName "kniip"
|
||||
#define AppExe "Kniip.exe"
|
||||
#ifndef AppVersion
|
||||
#define AppVersion "1.1.7"
|
||||
#define AppVersion "1.1.9"
|
||||
#endif
|
||||
#ifndef PublishDir
|
||||
#define PublishDir "..\artifacts\publish"
|
||||
|
||||
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 |
@@ -427,11 +427,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)
|
||||
|
||||
@@ -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(0xD9, 0xD9, 0xD9)));
|
||||
|
||||
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;
|
||||
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(ft, new Point(Position.X + pad, Position.Y + pad));
|
||||
dc.DrawText(shadow, new Point(layout.Origin.X + 1.5, layout.Origin.Y + 1.5));
|
||||
}
|
||||
|
||||
public override void Move(Vector delta) => Position += delta;
|
||||
dc.DrawText(layout.Text, layout.Origin);
|
||||
}
|
||||
|
||||
/// <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,69 @@
|
||||
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 { Vertical, Horizontal, Diagonal, 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>
|
||||
public sealed record CanvasBackground(
|
||||
BackgroundKind Kind,
|
||||
Color Color,
|
||||
Color Color2,
|
||||
GradientDirection Direction,
|
||||
BitmapSource? Image)
|
||||
{
|
||||
public static readonly CanvasBackground Default = new(
|
||||
BackgroundKind.None, Colors.White, Color.FromRgb(0x00, 0x78, 0xD4), GradientDirection.Vertical, 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() => Direction switch
|
||||
{
|
||||
GradientDirection.Horizontal => new LinearGradientBrush(Color, Color2, new Point(0, 0.5), new Point(1, 0.5)),
|
||||
GradientDirection.Diagonal => new LinearGradientBrush(Color, Color2, new Point(0, 0), new Point(1, 1)),
|
||||
GradientDirection.Radial => new RadialGradientBrush(Color, Color2),
|
||||
_ => new LinearGradientBrush(Color, Color2, new Point(0.5, 0), new Point(0.5, 1))
|
||||
};
|
||||
|
||||
/// <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,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,75 @@
|
||||
</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="#C8C8C8" 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>
|
||||
|
||||
<!-- Richting van het verloop -->
|
||||
<DataTemplate x:Key="GradientDirectionTemplate">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Path x:Name="Icon" Data="M9,2.5 V15.5 M5,11.5 L9,15.5 L13,11.5" Stroke="{DynamicResource InkBrush}" StrokeThickness="1.4"
|
||||
StrokeLineJoin="Round" StrokeStartLineCap="Round" StrokeEndLineCap="Round" Width="18" Height="18" VerticalAlignment="Center" />
|
||||
<TextBlock x:Name="Label" Text="Van boven naar onder" Margin="8,0,0,0" VerticalAlignment="Center" FontSize="12" />
|
||||
</StackPanel>
|
||||
<DataTemplate.Triggers>
|
||||
<DataTrigger Binding="{Binding}" Value="{x:Static local:GradientDirection.Horizontal}">
|
||||
<Setter TargetName="Icon" Property="Data" Value="M2.5,9 H15.5 M11.5,5 L15.5,9 L11.5,13" />
|
||||
<Setter TargetName="Label" Property="Text" Value="Van links naar rechts" />
|
||||
</DataTrigger>
|
||||
<DataTrigger Binding="{Binding}" Value="{x:Static local:GradientDirection.Diagonal}">
|
||||
<Setter TargetName="Icon" Property="Data" Value="M3.5,3.5 L14.5,14.5 M8.5,14.5 H14.5 V8.5" />
|
||||
<Setter TargetName="Label" Property="Text" Value="Diagonaal" />
|
||||
</DataTrigger>
|
||||
<DataTrigger Binding="{Binding}" Value="{x:Static local:GradientDirection.Radial}">
|
||||
<Setter TargetName="Icon" Property="Data" Value="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" />
|
||||
<Setter TargetName="Label" Property="Text" Value="Vanuit het midden" />
|
||||
</DataTrigger>
|
||||
<DataTrigger Binding="{Binding RelativeSource={RelativeSource AncestorType=ComboBoxItem}}" Value="{x:Null}">
|
||||
<Setter TargetName="Label" Property="Visibility" Value="Collapsed" />
|
||||
</DataTrigger>
|
||||
</DataTemplate.Triggers>
|
||||
</DataTemplate>
|
||||
</Window.Resources>
|
||||
<DockPanel>
|
||||
<!-- Statusbalk -->
|
||||
@@ -222,6 +397,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" />
|
||||
@@ -312,6 +491,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}" />
|
||||
@@ -366,6 +551,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,22 +631,165 @@
|
||||
</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" />
|
||||
<ComboBox x:Name="GradientDirectionCombo" Style="{StaticResource MiniCombo}" ItemTemplate="{StaticResource GradientDirectionTemplate}"
|
||||
ToolTip="Richting van het verloop" SelectionChanged="GradientDirection_Changed" />
|
||||
<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"
|
||||
<!-- 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" ToolTip="Rechtsklik om de hulplijnen te wissen"
|
||||
MouseRightButtonUp="RulerCorner_MouseRightButtonUp" />
|
||||
<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">
|
||||
<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" />
|
||||
@@ -459,5 +797,29 @@
|
||||
</Grid>
|
||||
</Grid>
|
||||
</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
@@ -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,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);
|
||||
|
||||
@@ -51,14 +51,14 @@ 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);
|
||||
@@ -100,7 +100,7 @@ internal sealed class TrayIcon : IDisposable
|
||||
{
|
||||
if (e.Button == MouseButtons.Left)
|
||||
{
|
||||
OpenFromClipboardRequested?.Invoke();
|
||||
SnipRequested?.Invoke();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||
<Product>kniip</Product>
|
||||
<Description>Annotatie-editor naast het Windows Knipprogramma</Description>
|
||||
<Version>1.1.7</Version>
|
||||
<Version>1.1.9</Version>
|
||||
<SatelliteResourceLanguages>nl;en</SatelliteResourceLanguages>
|
||||
<!-- WPF regelt DPI via app.manifest; de WinForms-analyzer is hier niet van toepassing. -->
|
||||
<NoWarn>$(NoWarn);WFAC010</NoWarn>
|
||||
|
||||
@@ -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
|
||||
@@ -122,8 +125,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; }
|
||||
|
||||
|
||||
@@ -239,6 +239,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." />
|
||||
|
||||
@@ -102,7 +102,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;
|
||||
|
||||
@@ -451,7 +458,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();
|
||||
|
||||
Reference in new issue
Block a user